diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index 6cad97ecc4456..eed4654725038 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -32,6 +32,7 @@ enabled: - x-pack/test/api_integration/apis/synthetics/config.ts - x-pack/test/api_integration/apis/uptime/config.ts - x-pack/test/api_integration/apis/entity_manager/config.ts + - x-pack/test/api_integration/apis/streams/config.ts - x-pack/test/apm_api_integration/basic/config.ts - x-pack/test/apm_api_integration/cloud/config.ts - x-pack/test/apm_api_integration/rules/config.ts diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index 92231d27ea50f..ffec5b75fca68 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -22,7 +22,7 @@ "@types/mocha": "^10.0.1", "@types/node": "^15.12.2", "chai": "^4.3.10", - "mocha": "^10.3.0", + "mocha": "^10.8.2", "nock": "^12.0.2", "ts-node": "^10.9.2", "typescript": "^5.1.6" @@ -280,6 +280,15 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -536,12 +545,12 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -590,9 +599,9 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -1042,9 +1051,9 @@ } }, "node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1053,31 +1062,31 @@ } }, "node_modules/mocha": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", - "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -1087,33 +1096,6 @@ "node": ">= 14.0.0" } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -1130,9 +1112,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/nock": { @@ -1567,9 +1549,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -1622,9 +1604,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" @@ -1893,6 +1875,12 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2090,12 +2078,12 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -2124,9 +2112,9 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, "dir-glob": { @@ -2438,62 +2426,41 @@ } }, "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "requires": { "brace-expansion": "^2.0.1" } }, "mocha": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", - "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", "serialize-javascript": "^6.0.2", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2506,9 +2473,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "nock": { @@ -2784,9 +2751,9 @@ } }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -2827,9 +2794,9 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, "yargs-unparser": { diff --git a/.buildkite/package.json b/.buildkite/package.json index 158a55c777e6a..5d5293971833a 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -24,7 +24,7 @@ "@types/mocha": "^10.0.1", "@types/node": "^15.12.2", "chai": "^4.3.10", - "mocha": "^10.3.0", + "mocha": "^10.8.2", "nock": "^12.0.2", "ts-node": "^10.9.2", "typescript": "^5.1.6" diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index c88e37490eb43..ca454f64c2696 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -15,6 +15,7 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-codeql.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-console-definitions-sync.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-deploy-project.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml diff --git a/.buildkite/scripts/steps/console_definitions_sync.sh b/.buildkite/scripts/steps/console_definitions_sync.sh index 55719292959e8..7dc565e0b9642 100755 --- a/.buildkite/scripts/steps/console_definitions_sync.sh +++ b/.buildkite/scripts/steps/console_definitions_sync.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +GIT_SCOPE="src/plugins/console/server/lib/spec_definitions" + report_main_step () { echo "--- $1" } @@ -16,14 +18,16 @@ main () { exit 1 fi + report_main_step "Bootstrapping Kibana" cd "$KIBANA_DIR" + .buildkite/scripts/bootstrap.sh report_main_step "Generating console definitions" node scripts/generate_console_definitions.js --source "$PARENT_DIR/elasticsearch-specification" --emptyDest # Check if there are any differences set +e - git diff --exit-code --quiet "$destination_file" + git diff --exit-code --quiet "$GIT_SCOPE" if [ $? -eq 0 ]; then echo "No differences found. Exiting.." exit @@ -54,7 +58,7 @@ main () { git checkout -b "$BRANCH_NAME" - git add src/plugins/console/server/lib/spec_definitions/json/generated/* + git add $GIT_SCOPE git commit -m "Update console definitions" report_main_step "Changes committed. Creating pull request." diff --git a/.eslintrc.js b/.eslintrc.js index e2d02c33288a7..7ff37b0c9fd98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -952,6 +952,7 @@ module.exports = { { files: [ 'x-pack/plugins/observability_solution/**/*.{ts,tsx}', + 'x-pack/plugins/{streams,streams_app}/**/*.{ts,tsx}', 'x-pack/packages/observability/**/*.{ts,tsx}', ], rules: { @@ -959,7 +960,7 @@ module.exports = { 'error', { additionalHooks: - '^(useAbortableAsync|useMemoWithAbortSignal|useFetcher|useProgressiveFetcher|useBreadcrumb|useAsync|useTimeRangeAsync|useAutoAbortedHttpClient)$', + '^(useAbortableAsync|useMemoWithAbortSignal|useFetcher|useProgressiveFetcher|useBreadcrumb|useAsync|useTimeRangeAsync|useAutoAbortedHttpClient|use.*Fetch)$', }, ], }, @@ -968,6 +969,7 @@ module.exports = { files: [ 'x-pack/plugins/aiops/**/*.tsx', 'x-pack/plugins/observability_solution/**/*.tsx', + 'x-pack/plugins/{streams,streams_app}/**/*.{ts,tsx}', 'src/plugins/ai_assistant_management/**/*.tsx', 'x-pack/packages/observability/**/*.{ts,tsx}', ], @@ -984,6 +986,7 @@ module.exports = { { files: [ 'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', + 'x-pack/plugins/{streams,streams_app}/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', 'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', 'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', ], @@ -2015,6 +2018,15 @@ module.exports = { '@kbn/imports/no_group_crossing_imports': 'warn', }, }, + { + files: ['packages/kbn-dependency-usage/**/*.{ts,tsx}'], + rules: { + // disabling it since package is a CLI tool + 'no-console': 'off', + // disabling it since package is marked as module and it requires extension for files written + '@kbn/imports/uniform_imports': 'off', + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 54cd5bf6bd85d..47bbcd64afaab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -329,6 +329,7 @@ packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-di packages/kbn-data-stream-adapter @elastic/security-threat-hunting packages/kbn-data-view-utils @elastic/kibana-data-discovery packages/kbn-datemath @elastic/kibana-data-discovery +packages/kbn-dependency-usage @elastic/kibana-security packages/kbn-dev-cli-errors @elastic/kibana-operations packages/kbn-dev-cli-runner @elastic/kibana-operations packages/kbn-dev-proc-runner @elastic/kibana-operations @@ -465,6 +466,7 @@ packages/kbn-rrule @elastic/response-ops packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team packages/kbn-safer-lodash-set @elastic/kibana-security packages/kbn-saved-objects-settings @elastic/appex-sharedux +packages/kbn-scout @elastic/appex-qa packages/kbn-screenshotting-server @elastic/appex-sharedux packages/kbn-search-api-keys-components @elastic/search-kibana packages/kbn-search-api-keys-server @elastic/search-kibana @@ -920,6 +922,7 @@ x-pack/plugins/observability_solution/apm_data_access @elastic/obs-knowledge-tea x-pack/plugins/observability_solution/apm/ftr_e2e @elastic/obs-ux-infra_services-team x-pack/plugins/observability_solution/dataset_quality @elastic/obs-ux-logs-team x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities +x-pack/plugins/observability_solution/entity_manager_app @elastic/obs-entities x-pack/plugins/observability_solution/exploratory_view @elastic/obs-ux-management-team x-pack/plugins/observability_solution/infra @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team x-pack/plugins/observability_solution/inventory @elastic/obs-ux-infra_services-team @@ -961,6 +964,7 @@ x-pack/plugins/search_indices @elastic/search-kibana x-pack/plugins/search_inference_endpoints @elastic/search-kibana x-pack/plugins/search_notebooks @elastic/search-kibana x-pack/plugins/search_playground @elastic/search-kibana +x-pack/plugins/search_solution/search_navigation @elastic/search-kibana x-pack/plugins/searchprofiler @elastic/kibana-management x-pack/plugins/security @elastic/kibana-security x-pack/plugins/security_solution @elastic/security-solution @@ -975,6 +979,7 @@ x-pack/plugins/spaces @elastic/kibana-security x-pack/plugins/stack_alerts @elastic/response-ops x-pack/plugins/stack_connectors @elastic/response-ops x-pack/plugins/streams @simianhacker @flash1293 @dgieselaar +x-pack/plugins/streams_app @simianhacker @flash1293 @dgieselaar x-pack/plugins/task_manager @elastic/response-ops x-pack/plugins/telemetry_collection_xpack @elastic/kibana-core x-pack/plugins/threat_intelligence @elastic/security-threat-hunting-investigations @@ -1045,6 +1050,23 @@ x-pack/test_serverless/api_integration/test_suites/common/platform_security @ela # Data Discovery +/x-pack/test/functional/fixtures/kbn_archiver/kibana_scripted_fields_on_logstash.json @elastic/kibana-data-discovery # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/functional/apps/discover/async_scripted_fields.ts#L35 +/x-pack/test/functional/es_archives/getting_started/shakespeare @elastic/kibana-data-discovery +/x-pack/test/functional/fixtures/kbn_archiver/discover @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/unmapped_fields.json @elastic/kibana-data-discovery # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/group7/_indexpattern_with_unmapped_fields.ts#L28 +/test/functional/fixtures/kbn_archiver/testlargestring.json @elastic/kibana-data-discovery # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/group5/_large_string.ts#L28 +/test/functional/fixtures/kbn_archiver/message_with_newline.json @elastic/kibana-data-discovery # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/classic/_doc_table_newline.ts#L26 +/test/functional/fixtures/kbn_archiver/invalid_scripted_field.json @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/index_pattern_without_timefield.json @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/discover @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/discover.json @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/date_nested.json @elastic/kibana-data-discovery +/test/functional/fixtures/kbn_archiver/date_* @elastic/kibana-data-discovery +/test/functional/fixtures/es_archiver/unmapped_fields @elastic/kibana-data-discovery # Assigned per the only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/group7/_indexpattern_with_unmapped_fields.ts#L26 +/test/functional/fixtures/es_archiver/message_with_newline @elastic/kibana-data-discovery # Assigned per the only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/classic/_doc_table_newline.ts#L24 +/test/functional/fixtures/es_archiver/hamlet @elastic/kibana-data-discovery # Assigned per the only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/discover/group5/_large_string.ts#L30 +/test/api_integration/fixtures/kbn_archiver/index_patterns @elastic/kibana-data-discovery +/test/api_integration/fixtures/es_archiver/index_patterns @elastic/kibana-data-discovery /test/functional/fixtures/es_archiver/alias @elastic/kibana-data-discovery /test/functional/page_objects/context_page.ts @elastic/kibana-data-discovery /test/functional/services/data_views.ts @elastic/kibana-data-discovery @@ -1117,6 +1139,16 @@ src/plugins/discover/public/context_awareness/profile_providers/security @elasti /x-pack/test/screenshot_creation @elastic/platform-docs # Visualizations +/x-pack/test/functional/fixtures/kbn_archiver/rollup @elastic/kibana-visualizations # Assigned per the only uses are in lens and tsvb tests +/x-pack/test/functional/fixtures/kbn_archiver/hybrid_dataview.json @elastic/kibana-visualizations # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/functional/apps/visualize/hybrid_visualization.ts#L20 +/x-pack/test/functional/es_archives/pre_calculated_histogram @elastic/kibana-visualizations # Assigned per usages +/x-pack/test/functional/es_archives/hybrid/rollup @elastic/kibana-visualizations @elastic/search-kibana # Assigned per usage +/x-pack/test/functional/es_archives/hybrid/logstash @elastic/kibana-visualizations # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/functional/apps/visualize/hybrid_visualization.ts#L22 +/x-pack/test/functional/es_archives/graph @elastic/kibana-visualizations +/x-pack/test/functional/es_archives/visualize @elastic/kibana-visualizations +/test/functional/fixtures/kbn_archiver/visualize.json @elastic/kibana-visualizations +/test/functional/fixtures/kbn_archiver/managed_content.json @elastic/kibana-visualizations # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/functional/apps/managed_content/managed_content.ts#L38 +/test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json @elastic/kibana-visualizations /test/functional/page_objects/unified_search_page.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/200132#discussion_r1847188168 /test/functional/apps/getting_started/*.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/199767#discussion_r1840485031 /x-pack/test/upgrade/apps/graph @elastic/kibana-visualizations @@ -1211,6 +1243,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/observability_ai_assistant_api_integration @elastic/obs-ai-assistant /x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant /x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai-assistant +/x-pack/test/functional/es_archives/observability/ai_assistant @elastic/obs-ai-assistant # Infra Obs ## This plugin mostly contains the codebase for the infra services, but also includes some code for the Logs UI app. @@ -1286,7 +1319,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/functional/apps/infra/logs @elastic/obs-ux-logs-team # Observability UX management team -/x-pack/test/api_integration/services/data_view_api.ts @elastic/obs-ux-management-team +/test/api_integration/apis/suggestions @elastic/obs-ux-management-team # Assigned per https://github.com/elastic/kibana/pull/200950#discussion_r1853705079 /x-pack/test/api_integration/services/slo.ts @elastic/obs-ux-management-team /x-pack/test/functional/services/slo @elastic/obs-ux-management-team /x-pack/test/functional/apps/slo @elastic/obs-ux-management-team @@ -1356,6 +1389,8 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team # obs-ux-logs-team +/x-pack/test/functional/es_archives/observability_logs_explorer @elastic/obs-ux-logs-team +/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/config.* @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/landing_page.ts @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/navigation.ts @elastic/obs-ux-logs-team @@ -1396,6 +1431,9 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql ### END Observability Plugins # Presentation +/x-pack/test/upgrade/screenshots @elastic/kibana-presentation +/x-pack/test/functional/screenshots @elastic/kibana-presentation +/test/functional/fixtures/kbn_archiver/legacy.json @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/200934#discussion_r1856407606 /x-pack/test/functional/fixtures/kbn_archiver/maps.json @elastic/kibana-presentation /x-pack/test/functional/fixtures/kbn_archiver/canvas @elastic/kibana-presentation /x-pack/test/functional/es_archives/dashboard/async_search @elastic/kibana-presentation @@ -1478,6 +1516,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/api_integration/services/transform.ts @elastic/ml-ui /x-pack/test/functional/apps/aiops @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui +/x-pack/test/functional/es_archives/large_arrays @elastic/ml-ui # Assigned per usages /x-pack/test/functional/services/transform/ @elastic/ml-ui /x-pack/test/functional/services/aiops @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui @@ -1514,6 +1553,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /.eslintignore @elastic/kibana-operations # QA - Appex QA +/x-pack/plugins/discover_enhanced/ui_tests/ @elastic/appex-qa # temporarily /x-pack/test/functional/fixtures/package_registry_config.yml @elastic/appex-qa # No usages found /x-pack/test/functional/fixtures/kbn_archiver/packaging.json @elastic/appex-qa # No usages found /x-pack/test/functional/es_archives/filebeat @elastic/appex-qa @@ -1660,6 +1700,16 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa # Core +/test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json @elastic/kibana-core @elastic/kibana-data-discovery +/x-pack/test/functional/es_archives/lists @elastic/kibana-core +/test/functional/fixtures/kbn_archiver/saved_search.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts#L100 +/test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/saved_objects_management/show_relationships.ts#L20 +/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_from_http_apis.json @elastic/kibana-core +/test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/saved_objects_management/inspect_saved_objects.ts#L40 +/test/functional/fixtures/es_archiver/saved_objects_management @elastic/kibana-core +/test/api_integration/fixtures/es_archiver/saved_objects @elastic/kibana-core +/test/api_integration/fixtures/kbn_archiver/saved_objects @elastic/kibana-core +/test/interpreter_functional @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/blob/main/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.jsonc#L4 /test/api_integration/apis/general/*.js @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/199795/files/894a8ede3f9d0398c5af56bf5a82654a9bc0610b#r1846691639 /x-pack/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-core /x-pack/test/functional/page_objects/navigational_search.ts @elastic/kibana-core @@ -1777,6 +1827,15 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib # Kibana Platform Security # security +/x-pack/test/functional/es_archives/security @elastic/kibana-security +/x-pack/test/functional/fixtures/kbn_archiver/spaces @elastic/kibana-security +/x-pack/test/functional/fixtures/kbn_archiver/security @elastic/kibana-security +/x-pack/test/ftr_apis/common/lib @elastic/kibana-security +/x-pack/test/ftr_apis/common/fixtures/es_archiver/base_data/space_1.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33 +/x-pack/test/ftr_apis/common/fixtures/es_archiver/base_data/default_space.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33 +/x-pack/test/api_integration/apis/cloud @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/198444 +/test/plugin_functional/snapshots/baseline/hardening/prototype.json @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/190716 +/test/functional/page_objects/login_page.ts @elastic/kibana-security /x-pack/test_serverless/functional/test_suites/observability/role_management @elastic/kibana-security /x-pack/test/functional/config_security_basic.ts @elastic/kibana-security /x-pack/test/functional/page_objects/user_profile_page.ts @elastic/kibana-security @@ -1903,6 +1962,12 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib # Enterprise Search # search +/x-pack/test/functional/es_archives/data/search_sessions @elastic/search-kibana +/x-pack/test/common/services/search_secure.ts @elastic/search-kibana +/test/functional/page_objects/unified_search_page.ts @elastic/search-kibana +/test/functional/fixtures/kbn_archiver/ccs @elastic/search-kibana +/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json @elastic/search-kibana +/test/functional/fixtures/es_archiver/search/downsampled @elastic/search-kibana /x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts @elastic/search-kibana /x-pack/test/functional/services/search_sessions.ts @elastic/search-kibana /x-pack/test/functional/page_objects/search_* @elastic/search-kibana @@ -1923,6 +1988,9 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test/functional_search/ @elastic/search-kibana # Management Experience - Deployment Management +/test/functional/fixtures/kbn_archiver/management.json @elastic/kibana-management @elastic/kibana-data-discovery # Assigned per 2 uses: test/functional/apps/management/_import_objects.ts && test/functional/apps/management/data_views/_scripted_fields_filter.ts +/x-pack/test/functional/fixtures/kbn_archiver/home/feature_controls/security/security.json @elastic/kibana-management +/x-pack/test/functional/es_archives/upgrade_assistant @elastic/kibana-management /x-pack/test/functional/services/ace_editor.js @elastic/kibana-management /x-pack/test/functional/page_objects/remote_clusters_page.ts @elastic/kibana-management /x-pack/test/stack_functional_integration/apps/ccs @elastic/kibana-management @@ -2080,6 +2148,8 @@ x-pack/test/security_solution_api_integration/test_suites/explore @elastic/secur x-pack/test/security_solution_api_integration/test_suites/investigations @elastic/security-threat-hunting-investigations x-pack/test/security_solution_api_integration/test_suites/sources @elastic/security-detections-response /x-pack/test/common/utils/security_solution/detections_response @elastic/security-detections-response +/x-pack/test/functional/es_archives/signals @elastic/security-detections-response +/x-pack/test/functional/es_archives/rule_keyword_family @elastic/security-detections-response # Security Solution sub teams @@ -2097,6 +2167,7 @@ x-pack/test/security_solution_api_integration/test_suites/sources @elastic/secur /x-pack/plugins/security_solution/server/lib/siem_migrations @elastic/security-threat-hunting /x-pack/plugins/security_solution/common/siem_migrations @elastic/security-threat-hunting +/x-pack/plugins/security_solution/public/siem_migrations @elastic/security-threat-hunting ## Security Solution Threat Hunting areas - Threat Hunting Investigations @@ -2338,7 +2409,8 @@ x-pack/plugins/security_solution/common/api/entity_analytics @elastic/security-e x-pack/test/security_solution_api_integration/test_suites/genai @elastic/security-generative-ai # Security Defend Workflows - OSQuery Ownership -x-pack/plugins/osquery @elastic/security-defend-workflows +/x-pack/test/osquery_cypress @elastic/security-defend-workflows +/x-pack/plugins/osquery @elastic/security-defend-workflows /x-pack/plugins/security_solution/common/api/detection_engine/model/rule_response_actions @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-defend-workflows /x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-defend-workflows @@ -2398,6 +2470,7 @@ x-pack/plugins/security_solution/server/lib/security_integrations @elastic/secur /x-pack/plugins/security_solution_serverless/**/*.scss @elastic/security-design # Logstash +/x-pack/test/functional/es_archives/logstash/example_pipelines @elastic/logstash /x-pack/test/functional/services/pipeline_* @elastic/logstash /x-pack/test/functional/page_objects/logstash_page.ts @elastic/logstash /x-pack/test/functional/apps/logstash @elastic/logstash diff --git a/.gitignore b/.gitignore index 9bda927a92b6a..be8d495f95f1d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ target *.iml *.log types.eslint.config.js +types.eslint.config.cjs __tmp__ # Ignore example plugin builds @@ -142,6 +143,8 @@ x-pack/test/security_api_integration/plugins/audit_log/audit.log .ftr role_users.json +# ignore Scout temp directory +.scout .devcontainer/.env @@ -159,3 +162,4 @@ x-pack/test/security_solution_playwright/playwright/.cache/ x-pack/test/security_solution_playwright/.auth/ x-pack/test/security_solution_playwright/.env .codeql +.dependency-graph-log.json diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index c46307e899555..3f07bf7bdcb12 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 3c758b28e1621..88238206f2298 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/ai_assistant_management_selection.mdx b/api_docs/ai_assistant_management_selection.mdx index 1f9bbdc82eafe..c90f024179352 100644 --- a/api_docs/ai_assistant_management_selection.mdx +++ b/api_docs/ai_assistant_management_selection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiAssistantManagementSelection title: "aiAssistantManagementSelection" image: https://source.unsplash.com/400x175/?github description: API docs for the aiAssistantManagementSelection plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiAssistantManagementSelection'] --- import aiAssistantManagementSelectionObj from './ai_assistant_management_selection.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 1b86116b3a61c..bcd45bce6e677 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 824a897b7edbb..eecd6de841e70 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index a288da3567466..d6b2c432c467d 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -695,7 +695,7 @@ "section": "def-common.TopNFunctions", "text": "TopNFunctions" }, - "; hostNames: string[]; } | undefined, ", + "; hostNames: string[]; containerIds: string[]; } | undefined, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/services/{serviceName}/profiling/hosts/flamegraph\": ", { @@ -829,7 +829,7 @@ "section": "def-common.BaseFlameGraph", "text": "BaseFlameGraph" }, - "; hostNames: string[]; } | undefined, ", + "; hostNames: string[]; containerIds: string[]; } | undefined, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/services/{serviceName}/profiling/functions\": ", { diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 3fd196f6a3ea1..9eef86e08c262 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/apm_data_access.mdx b/api_docs/apm_data_access.mdx index 70d67751d9f52..b02711293b20a 100644 --- a/api_docs/apm_data_access.mdx +++ b/api_docs/apm_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apmDataAccess title: "apmDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the apmDataAccess plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apmDataAccess'] --- import apmDataAccessObj from './apm_data_access.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index c4f3b9492fcd2..a1eca8cf25e6a 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 5354ef0b591ba..ce14885554f36 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index a012f754c98f6..dbc5d9250be32 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 279f5f92bc770..edf89128a5922 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index c45e0a8dead2c..962acd458c4ec 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 32379e22a8fbd..19cc7327b4ae8 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index 000e9143be76f..fef5a02c211d4 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index 54adbbaddee54..0eb94ddf351b2 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 82a7ab678f338..c6f808809f4dc 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index ca5595652c622..063f052714ecf 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index baec4a847cbee..af321e0558b6e 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index ed65a57c45b84..10201057da0db 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 168097f888727..b9a9aa508b0b1 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 896f30264a98d..ac9b56f9ed8a9 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 39ff8a3aed26d..60e0789235327 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 06c3e8522b3bd..01dd0d4c50713 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_quality.mdx b/api_docs/data_quality.mdx index 041afddaca06d..7b2c076579299 100644 --- a/api_docs/data_quality.mdx +++ b/api_docs/data_quality.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataQuality title: "dataQuality" image: https://source.unsplash.com/400x175/?github description: API docs for the dataQuality plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataQuality'] --- import dataQualityObj from './data_quality.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index ba24c3db6c879..113c8c9356fbb 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 52bf12c0a388b..57cd0c01eab73 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_usage.devdocs.json b/api_docs/data_usage.devdocs.json index 09fcffd5c58eb..fa0dd73eb8c27 100644 --- a/api_docs/data_usage.devdocs.json +++ b/api_docs/data_usage.devdocs.json @@ -88,7 +88,7 @@ "signature": [ "\"/api/data_usage/\"" ], - "path": "x-pack/plugins/data_usage/common/index.ts", + "path": "x-pack/plugins/data_usage/common/constants.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -100,7 +100,7 @@ "tags": [], "label": "DATA_USAGE_DATA_STREAMS_API_ROUTE", "description": [], - "path": "x-pack/plugins/data_usage/common/index.ts", + "path": "x-pack/plugins/data_usage/common/constants.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -112,7 +112,7 @@ "tags": [], "label": "DATA_USAGE_METRICS_API_ROUTE", "description": [], - "path": "x-pack/plugins/data_usage/common/index.ts", + "path": "x-pack/plugins/data_usage/common/constants.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -127,7 +127,7 @@ "signature": [ "50" ], - "path": "x-pack/plugins/data_usage/common/index.ts", + "path": "x-pack/plugins/data_usage/common/constants.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -142,19 +142,7 @@ "signature": [ "\"data_usage\"" ], - "path": "x-pack/plugins/data_usage/common/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dataUsage", - "id": "def-common.PLUGIN_NAME", - "type": "string", - "tags": [], - "label": "PLUGIN_NAME", - "description": [], - "path": "x-pack/plugins/data_usage/common/index.ts", + "path": "x-pack/plugins/data_usage/common/constants.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false diff --git a/api_docs/data_usage.mdx b/api_docs/data_usage.mdx index 3e2caf4ea20f7..5632d8c4e7d3c 100644 --- a/api_docs/data_usage.mdx +++ b/api_docs/data_usage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataUsage title: "dataUsage" image: https://source.unsplash.com/400x175/?github description: API docs for the dataUsage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataUsage'] --- import dataUsageObj from './data_usage.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 10 | 0 | 10 | 0 | +| 9 | 0 | 9 | 0 | ## Client diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index eda90bd90b1ae..e2cc2fc3ff03e 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index a40a06de84f75..81ae08552215b 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index d611d8e1d6c48..c6f99faaab2b9 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 17af467c336c4..2db8e5ec94763 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -14202,10 +14202,6 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/eql_query_bar/validators.ts" - }, { "plugin": "timelines", "path": "x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts" diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index e3b18891387da..3840c930093df 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 10b17f6cb009b..2a3a70d6c5cb9 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/dataset_quality.devdocs.json b/api_docs/dataset_quality.devdocs.json index 0f42873f1c07f..f6d641e428113 100644 --- a/api_docs/dataset_quality.devdocs.json +++ b/api_docs/dataset_quality.devdocs.json @@ -561,7 +561,7 @@ "section": "def-common.ServerRouteCreateOptions", "text": "ServerRouteCreateOptions" }, - "> ? TRouteParamsRT extends ", + " | undefined> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -912,15 +912,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - " ? TReturnType extends ", + " ? TReturnType extends ", { "pluginId": "@kbn/core-http-server", "scope": "server", diff --git a/api_docs/dataset_quality.mdx b/api_docs/dataset_quality.mdx index 8b5a77fa9f8ba..27735e46c00c2 100644 --- a/api_docs/dataset_quality.mdx +++ b/api_docs/dataset_quality.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/datasetQuality title: "datasetQuality" image: https://source.unsplash.com/400x175/?github description: API docs for the datasetQuality plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'datasetQuality'] --- import datasetQualityObj from './dataset_quality.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 93404d07f9595..c3582dbe17f1f 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 0afb9d9eeaefa..be7588db27ddb 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -1328,7 +1328,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.ts#:~:text=create) | - | | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts#:~:text=options) | - | -| | [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title), [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title), [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/eql_query_bar/validators.ts#:~:text=title) | - | +| | [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title), [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title), [create_sourcerer_data_view.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts#:~:text=title) | - | | | [fleet_services.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts#:~:text=policy_id), [fleet_services.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts#:~:text=policy_id), [endpoint_metadata_service.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts#:~:text=policy_id), [endpoint_package_policies.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/endpoint_package_policies.test.ts#:~:text=policy_id), [index.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts#:~:text=policy_id) | - | | | [fleet_services.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts#:~:text=policy_id), [fleet_services.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts#:~:text=policy_id), [endpoint_metadata_service.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts#:~:text=policy_id), [endpoint_package_policies.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/endpoint_package_policies.test.ts#:~:text=policy_id), [index.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts#:~:text=policy_id) | - | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index f6fb16069c438..58fe13364fbbd 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 014e3b9577291..82e50bc914279 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index e51f0efbca904..339ab780f9c62 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 8f33990a5cb6a..e565b8349addc 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/discover_shared.mdx b/api_docs/discover_shared.mdx index 5625ea4ce73a1..3446bd7676e0d 100644 --- a/api_docs/discover_shared.mdx +++ b/api_docs/discover_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverShared title: "discoverShared" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverShared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverShared'] --- import discoverSharedObj from './discover_shared.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index a89da9995ee88..d95004e7f4aca 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/elastic_assistant.mdx b/api_docs/elastic_assistant.mdx index 67ebc7fedb139..54a208c08fb0c 100644 --- a/api_docs/elastic_assistant.mdx +++ b/api_docs/elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/elasticAssistant title: "elasticAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the elasticAssistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'elasticAssistant'] --- import elasticAssistantObj from './elastic_assistant.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index cb4b3c4bad40d..3f410c13fc6eb 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index e7c8c82ff0292..5be5ced4c35aa 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index acb4cefaddec0..ffcb619986b57 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index e7a15f22d30a5..cce28be65864a 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/entities_data_access.mdx b/api_docs/entities_data_access.mdx index d3dc9b4f8bf3f..82e53658be8c3 100644 --- a/api_docs/entities_data_access.mdx +++ b/api_docs/entities_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/entitiesDataAccess title: "entitiesDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the entitiesDataAccess plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'entitiesDataAccess'] --- import entitiesDataAccessObj from './entities_data_access.devdocs.json'; diff --git a/api_docs/entity_manager.devdocs.json b/api_docs/entity_manager.devdocs.json index a3f5fff48eb99..7895ae318fb61 100644 --- a/api_docs/entity_manager.devdocs.json +++ b/api_docs/entity_manager.devdocs.json @@ -2,6 +2,993 @@ "id": "entityManager", "client": { "classes": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient", + "type": "Class", + "tags": [], + "label": "EntityClient", + "description": [], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.repositoryClient", + "type": "Function", + "tags": [], + "label": "repositoryClient", + "description": [], + "signature": [ + "(endpoint: TEndpoint, ...args: MaybeOptionalArgs<", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ClientRequestParamsOf", + "text": "ClientRequestParamsOf" + }, + "<{ \"POST /internal/entities/v2/_search/preview\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search/preview\", Zod.ZodObject<{ body: Zod.ZodObject<{ sources: Zod.ZodArray>; index_patterns: Zod.ZodArray; identity_fields: Zod.ZodArray; metadata_fields: Zod.ZodArray; filters: Zod.ZodArray; }, \"strip\", Zod.ZodTypeAny, { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }, { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }>, \"many\">; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }, { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }; }, { body: { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + "<{ entities: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.EntityV2", + "text": "EntityV2" + }, + "[]; }>, undefined>; \"POST /internal/entities/v2/_search\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search\", Zod.ZodObject<{ body: Zod.ZodObject<{ type: Zod.ZodString; metadata_fields: Zod.ZodDefault>>; filters: Zod.ZodDefault>>; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }, { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }; }, { body: { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"PATCH /internal/entities/definition/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PATCH /internal/entities/definition/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject; filter: Zod.ZodOptional>; version: Zod.ZodOptional>; name: Zod.ZodOptional; description: Zod.ZodOptional>; metrics: Zod.ZodOptional; field: Zod.ZodString; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"doc_count\">; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"percentile\">; field: Zod.ZodString; percentile: Zod.ZodNumber; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }>]>, \"many\">; equation: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }>, \"many\">>>; indexPatterns: Zod.ZodOptional>; metadata: Zod.ZodOptional; aggregation: Zod.ZodDefault; limit: Zod.ZodDefault; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; }, { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"top_value\">; sort: Zod.ZodRecord, Zod.ZodLiteral<\"desc\">]>>; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }>]>>>; }, \"strip\", Zod.ZodTypeAny, { source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; destination?: string | undefined; }, { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, Zod.ZodEffects]>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, \"many\">>>; identityFields: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { field: string; optional: false; }, { field: string; optional: false; }>, Zod.ZodEffects]>, \"many\">>; displayNameTemplate: Zod.ZodOptional; staticFields: Zod.ZodOptional>>; latest: Zod.ZodOptional>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>; }, \"strip\", Zod.ZodTypeAny, { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }, { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }>>; installedComponents: Zod.ZodOptional, Zod.ZodLiteral<\"ingest_pipeline\">, Zod.ZodLiteral<\"template\">]>; id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }>, \"many\">>>; }, { latest: Zod.ZodOptional; lookbackPeriod: Zod.ZodOptional>>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>>; }, \"strip\", Zod.ZodTypeAny, { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; }, { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; }>>; version: Zod.ZodEffects; }>, \"strip\", Zod.ZodTypeAny, { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; identityFields?: ({ field: string; optional: false; } | { field: string; optional: boolean; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }, { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; identityFields?: (string | { field: string; optional: false; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; body: { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; identityFields?: ({ field: string; optional: false; } | { field: string; optional: boolean; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }, { path: { id: string; }; body: { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; identityFields?: (string | { field: string; optional: false; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"POST /internal/entities/definition/{id}/_reset\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/definition/{id}/_reset\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"GET /internal/entities/definition/{id?}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /internal/entities/definition/{id?}\", Zod.ZodObject<{ query: Zod.ZodObject<{ page: Zod.ZodOptional; perPage: Zod.ZodOptional; includeState: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { includeState: boolean; page?: number | undefined; perPage?: number | undefined; }, { page?: number | undefined; perPage?: number | undefined; includeState?: boolean | \"true\" | \"false\" | undefined; }>; path: Zod.ZodObject<{ id: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { id?: string | undefined; }, { id?: string | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { includeState: boolean; page?: number | undefined; perPage?: number | undefined; }; path: { id?: string | undefined; }; }, { query: { page?: number | undefined; perPage?: number | undefined; includeState?: boolean | \"true\" | \"false\" | undefined; }; path: { id?: string | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"DELETE /internal/entities/definition/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"DELETE /internal/entities/definition/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; query: Zod.ZodObject<{ deleteData: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { deleteData: boolean; }, { deleteData?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { deleteData: boolean; }; path: { id: string; }; }, { query: { deleteData?: boolean | \"true\" | \"false\" | undefined; }; path: { id: string; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"POST /internal/entities/definition\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/definition\", Zod.ZodObject<{ query: Zod.ZodObject<{ installOnly: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { installOnly: boolean; }, { installOnly?: boolean | \"true\" | \"false\" | undefined; }>; body: Zod.ZodObject<{ id: Zod.ZodString; version: Zod.ZodEffects; name: Zod.ZodString; description: Zod.ZodOptional; type: Zod.ZodString; filter: Zod.ZodOptional; indexPatterns: Zod.ZodArray; identityFields: Zod.ZodArray; }, \"strip\", Zod.ZodTypeAny, { field: string; optional: false; }, { field: string; optional: false; }>, Zod.ZodEffects]>, \"many\">; displayNameTemplate: Zod.ZodString; metadata: Zod.ZodOptional; aggregation: Zod.ZodDefault; limit: Zod.ZodDefault; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; }, { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"top_value\">; sort: Zod.ZodRecord, Zod.ZodLiteral<\"desc\">]>>; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }>]>>>; }, \"strip\", Zod.ZodTypeAny, { source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; destination?: string | undefined; }, { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, Zod.ZodEffects]>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, \"many\">>; metrics: Zod.ZodOptional; field: Zod.ZodString; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"doc_count\">; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"percentile\">; field: Zod.ZodString; percentile: Zod.ZodNumber; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }>]>, \"many\">; equation: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }>, \"many\">>; staticFields: Zod.ZodOptional>; managed: Zod.ZodDefault>; latest: Zod.ZodObject<{ timestampField: Zod.ZodString; lookbackPeriod: Zod.ZodDefault>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>; }, \"strip\", Zod.ZodTypeAny, { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }, { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }>; installStatus: Zod.ZodOptional, Zod.ZodLiteral<\"upgrading\">, Zod.ZodLiteral<\"installed\">, Zod.ZodLiteral<\"failed\">]>>; installStartedAt: Zod.ZodOptional; installedComponents: Zod.ZodOptional, Zod.ZodLiteral<\"ingest_pipeline\">, Zod.ZodLiteral<\"template\">]>; id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }>, \"many\">>; }, \"strip\", Zod.ZodTypeAny, { id: string; type: string; version: string; name: string; managed: boolean; indexPatterns: string[]; identityFields: ({ field: string; optional: false; } | { field: string; optional: boolean; })[]; displayNameTemplate: string; latest: { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }, { id: string; type: string; version: string; name: string; indexPatterns: string[]; identityFields: (string | { field: string; optional: false; })[]; displayNameTemplate: string; latest: { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; managed?: boolean | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { installOnly: boolean; }; body: { id: string; type: string; version: string; name: string; managed: boolean; indexPatterns: string[]; identityFields: ({ field: string; optional: false; } | { field: string; optional: boolean; })[]; displayNameTemplate: string; latest: { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }, { query: { installOnly?: boolean | \"true\" | \"false\" | undefined; }; body: { id: string; type: string; version: string; name: string; indexPatterns: string[]; identityFields: (string | { field: string; optional: false; })[]; displayNameTemplate: string; latest: { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; managed?: boolean | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"DELETE /internal/entities/managed/enablement\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"DELETE /internal/entities/managed/enablement\", Zod.ZodObject<{ query: Zod.ZodObject<{ deleteData: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { deleteData: boolean; }, { deleteData?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { deleteData: boolean; }; }, { query: { deleteData?: boolean | \"true\" | \"false\" | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"PUT /internal/entities/managed/enablement\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PUT /internal/entities/managed/enablement\", Zod.ZodObject<{ query: Zod.ZodObject<{ installOnly: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { installOnly: boolean; }, { installOnly?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { installOnly: boolean; }; }, { query: { installOnly?: boolean | \"true\" | \"false\" | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"GET /internal/entities/managed/enablement\": { endpoint: \"GET /internal/entities/managed/enablement\"; handler: ServerRouteHandler<", + "EntityManagerRouteHandlerResources", + ", undefined, ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ">; security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; }; }, TEndpoint> & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => Promise<", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ReturnOf", + "text": "ReturnOf" + }, + "<{ \"POST /internal/entities/v2/_search/preview\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search/preview\", Zod.ZodObject<{ body: Zod.ZodObject<{ sources: Zod.ZodArray>; index_patterns: Zod.ZodArray; identity_fields: Zod.ZodArray; metadata_fields: Zod.ZodArray; filters: Zod.ZodArray; }, \"strip\", Zod.ZodTypeAny, { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }, { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }>, \"many\">; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }, { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }; }, { body: { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + "<{ entities: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.EntityV2", + "text": "EntityV2" + }, + "[]; }>, undefined>; \"POST /internal/entities/v2/_search\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search\", Zod.ZodObject<{ body: Zod.ZodObject<{ type: Zod.ZodString; metadata_fields: Zod.ZodDefault>>; filters: Zod.ZodDefault>>; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }, { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }; }, { body: { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"PATCH /internal/entities/definition/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PATCH /internal/entities/definition/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject; filter: Zod.ZodOptional>; version: Zod.ZodOptional>; name: Zod.ZodOptional; description: Zod.ZodOptional>; metrics: Zod.ZodOptional; field: Zod.ZodString; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"doc_count\">; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"percentile\">; field: Zod.ZodString; percentile: Zod.ZodNumber; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }>]>, \"many\">; equation: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }>, \"many\">>>; indexPatterns: Zod.ZodOptional>; metadata: Zod.ZodOptional; aggregation: Zod.ZodDefault; limit: Zod.ZodDefault; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; }, { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"top_value\">; sort: Zod.ZodRecord, Zod.ZodLiteral<\"desc\">]>>; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }>]>>>; }, \"strip\", Zod.ZodTypeAny, { source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; destination?: string | undefined; }, { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, Zod.ZodEffects]>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, \"many\">>>; identityFields: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { field: string; optional: false; }, { field: string; optional: false; }>, Zod.ZodEffects]>, \"many\">>; displayNameTemplate: Zod.ZodOptional; staticFields: Zod.ZodOptional>>; latest: Zod.ZodOptional>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>; }, \"strip\", Zod.ZodTypeAny, { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }, { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }>>; installedComponents: Zod.ZodOptional, Zod.ZodLiteral<\"ingest_pipeline\">, Zod.ZodLiteral<\"template\">]>; id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }>, \"many\">>>; }, { latest: Zod.ZodOptional; lookbackPeriod: Zod.ZodOptional>>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>>; }, \"strip\", Zod.ZodTypeAny, { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; }, { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; }>>; version: Zod.ZodEffects; }>, \"strip\", Zod.ZodTypeAny, { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; identityFields?: ({ field: string; optional: false; } | { field: string; optional: boolean; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }, { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; identityFields?: (string | { field: string; optional: false; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; body: { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; identityFields?: ({ field: string; optional: false; } | { field: string; optional: boolean; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }, { path: { id: string; }; body: { version: string; type?: string | undefined; filter?: string | undefined; name?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; indexPatterns?: string[] | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; identityFields?: (string | { field: string; optional: false; })[] | undefined; displayNameTemplate?: string | undefined; staticFields?: Record | undefined; latest?: { settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; timestampField?: string | undefined; } | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"POST /internal/entities/definition/{id}/_reset\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/definition/{id}/_reset\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"GET /internal/entities/definition/{id?}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /internal/entities/definition/{id?}\", Zod.ZodObject<{ query: Zod.ZodObject<{ page: Zod.ZodOptional; perPage: Zod.ZodOptional; includeState: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { includeState: boolean; page?: number | undefined; perPage?: number | undefined; }, { page?: number | undefined; perPage?: number | undefined; includeState?: boolean | \"true\" | \"false\" | undefined; }>; path: Zod.ZodObject<{ id: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { id?: string | undefined; }, { id?: string | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { includeState: boolean; page?: number | undefined; perPage?: number | undefined; }; path: { id?: string | undefined; }; }, { query: { page?: number | undefined; perPage?: number | undefined; includeState?: boolean | \"true\" | \"false\" | undefined; }; path: { id?: string | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"DELETE /internal/entities/definition/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"DELETE /internal/entities/definition/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; query: Zod.ZodObject<{ deleteData: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { deleteData: boolean; }, { deleteData?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { deleteData: boolean; }; path: { id: string; }; }, { query: { deleteData?: boolean | \"true\" | \"false\" | undefined; }; path: { id: string; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"POST /internal/entities/definition\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/definition\", Zod.ZodObject<{ query: Zod.ZodObject<{ installOnly: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { installOnly: boolean; }, { installOnly?: boolean | \"true\" | \"false\" | undefined; }>; body: Zod.ZodObject<{ id: Zod.ZodString; version: Zod.ZodEffects; name: Zod.ZodString; description: Zod.ZodOptional; type: Zod.ZodString; filter: Zod.ZodOptional; indexPatterns: Zod.ZodArray; identityFields: Zod.ZodArray; }, \"strip\", Zod.ZodTypeAny, { field: string; optional: false; }, { field: string; optional: false; }>, Zod.ZodEffects]>, \"many\">; displayNameTemplate: Zod.ZodString; metadata: Zod.ZodOptional; aggregation: Zod.ZodDefault; limit: Zod.ZodDefault; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; }, { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"top_value\">; sort: Zod.ZodRecord, Zod.ZodLiteral<\"desc\">]>>; lookbackPeriod: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }, { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }>]>>>; }, \"strip\", Zod.ZodTypeAny, { source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; destination?: string | undefined; }, { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, Zod.ZodEffects]>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; }, string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; }>, \"many\">>; metrics: Zod.ZodOptional; field: Zod.ZodString; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }, { name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"doc_count\">; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }, { name: string; aggregation: \"doc_count\"; filter?: string | undefined; }>, Zod.ZodObject<{ name: Zod.ZodString; aggregation: Zod.ZodLiteral<\"percentile\">; field: Zod.ZodString; percentile: Zod.ZodNumber; filter: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }, { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; }>]>, \"many\">; equation: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }, { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }>, \"many\">>; staticFields: Zod.ZodOptional>; managed: Zod.ZodDefault>; latest: Zod.ZodObject<{ timestampField: Zod.ZodString; lookbackPeriod: Zod.ZodDefault>; settings: Zod.ZodOptional; syncDelay: Zod.ZodOptional; frequency: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }, { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; }>>; }, \"strip\", Zod.ZodTypeAny, { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }, { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }>; installStatus: Zod.ZodOptional, Zod.ZodLiteral<\"upgrading\">, Zod.ZodLiteral<\"installed\">, Zod.ZodLiteral<\"failed\">]>>; installStartedAt: Zod.ZodOptional; installedComponents: Zod.ZodOptional, Zod.ZodLiteral<\"ingest_pipeline\">, Zod.ZodLiteral<\"template\">]>; id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }, { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }>, \"many\">>; }, \"strip\", Zod.ZodTypeAny, { id: string; type: string; version: string; name: string; managed: boolean; indexPatterns: string[]; identityFields: ({ field: string; optional: false; } | { field: string; optional: boolean; })[]; displayNameTemplate: string; latest: { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }, { id: string; type: string; version: string; name: string; indexPatterns: string[]; identityFields: (string | { field: string; optional: false; })[]; displayNameTemplate: string; latest: { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; managed?: boolean | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { installOnly: boolean; }; body: { id: string; type: string; version: string; name: string; managed: boolean; indexPatterns: string[]; identityFields: ({ field: string; optional: false; } | { field: string; optional: boolean; })[]; displayNameTemplate: string; latest: { lookbackPeriod: string; timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; metadata?: ({ destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; }; } | { destination: string; source: string; aggregation: { type: \"terms\"; limit: number; lookbackPeriod: undefined; }; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }, { query: { installOnly?: boolean | \"true\" | \"false\" | undefined; }; body: { id: string; type: string; version: string; name: string; indexPatterns: string[]; identityFields: (string | { field: string; optional: false; })[]; displayNameTemplate: string; latest: { timestampField: string; settings?: { frequency?: string | undefined; syncField?: string | undefined; syncDelay?: string | undefined; } | undefined; lookbackPeriod?: string | undefined; }; filter?: string | undefined; description?: string | undefined; metrics?: { name: string; metrics: ({ name: string; field: string; aggregation: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.BasicAggregations", + "text": "BasicAggregations" + }, + "; filter?: string | undefined; } | { name: string; aggregation: \"doc_count\"; filter?: string | undefined; } | { name: string; field: string; percentile: number; aggregation: \"percentile\"; filter?: string | undefined; })[]; equation: string; }[] | undefined; managed?: boolean | undefined; metadata?: (string | { source: string; destination?: string | undefined; aggregation?: { type: \"terms\"; limit?: number | undefined; lookbackPeriod?: string | undefined; } | { type: \"top_value\"; sort: Record; lookbackPeriod?: string | undefined; } | undefined; })[] | undefined; staticFields?: Record | undefined; installStatus?: \"failed\" | \"installing\" | \"upgrading\" | \"installed\" | undefined; installStartedAt?: string | undefined; installedComponents?: { id: string; type: \"transform\" | \"template\" | \"ingest_pipeline\"; }[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"DELETE /internal/entities/managed/enablement\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"DELETE /internal/entities/managed/enablement\", Zod.ZodObject<{ query: Zod.ZodObject<{ deleteData: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { deleteData: boolean; }, { deleteData?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { deleteData: boolean; }; }, { query: { deleteData?: boolean | \"true\" | \"false\" | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"PUT /internal/entities/managed/enablement\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PUT /internal/entities/managed/enablement\", Zod.ZodObject<{ query: Zod.ZodObject<{ installOnly: Zod.ZodDefault, Zod.ZodBoolean]>, boolean, boolean | \"true\" | \"false\">>>; }, \"strip\", Zod.ZodTypeAny, { installOnly: boolean; }, { installOnly?: boolean | \"true\" | \"false\" | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { query: { installOnly: boolean; }; }, { query: { installOnly?: boolean | \"true\" | \"false\" | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"GET /internal/entities/managed/enablement\": { endpoint: \"GET /internal/entities/managed/enablement\"; handler: ServerRouteHandler<", + "EntityManagerRouteHandlerResources", + ", undefined, ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ">; security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; }; }, TEndpoint>>" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.repositoryClient.$1", + "type": "Uncategorized", + "tags": [], + "label": "endpoint", + "description": [], + "signature": [ + "TEndpoint" + ], + "path": "packages/kbn-server-route-repository-utils/src/typings.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.repositoryClient.$2", + "type": "Uncategorized", + "tags": [], + "label": "args", + "description": [], + "signature": [ + "RequiredKeys", + "<", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ClientRequestParamsOf", + "text": "ClientRequestParamsOf" + }, + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "> extends never ? [] | [", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ClientRequestParamsOf", + "text": "ClientRequestParamsOf" + }, + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "] : [", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ClientRequestParamsOf", + "text": "ClientRequestParamsOf" + }, + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "]" + ], + "path": "packages/kbn-server-route-repository-utils/src/typings.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.Unnamed.$1", + "type": "CompoundType", + "tags": [], + "label": "core", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-lifecycle-browser", + "scope": "public", + "docId": "kibKbnCoreLifecycleBrowserPluginApi", + "section": "def-public.CoreStart", + "text": "CoreStart" + }, + " | ", + { + "pluginId": "@kbn/core-lifecycle-browser", + "scope": "public", + "docId": "kibKbnCoreLifecycleBrowserPluginApi", + "section": "def-public.CoreSetup", + "text": "CoreSetup" + }, + "" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.isManagedEntityDiscoveryEnabled", + "type": "Function", + "tags": [], + "label": "isManagedEntityDiscoveryEnabled", + "description": [], + "signature": [ + "() => Promise<{ enabled: boolean; reason: string; }>" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.enableManagedEntityDiscovery", + "type": "Function", + "tags": [], + "label": "enableManagedEntityDiscovery", + "description": [], + "signature": [ + "(query?: { installOnly?: boolean | \"true\" | \"false\" | undefined; } | undefined) => Promise<{ success: boolean; reason: string; message: string; }>" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.enableManagedEntityDiscovery.$1", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "signature": [ + "{ installOnly?: boolean | \"true\" | \"false\" | undefined; } | undefined" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.disableManagedEntityDiscovery", + "type": "Function", + "tags": [], + "label": "disableManagedEntityDiscovery", + "description": [], + "signature": [ + "(query?: { deleteData?: boolean | \"true\" | \"false\" | undefined; } | undefined) => Promise<{ success: boolean; reason: string; message: string; }>" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.disableManagedEntityDiscovery.$1", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "signature": [ + "{ deleteData?: boolean | \"true\" | \"false\" | undefined; } | undefined" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.asKqlFilter", + "type": "Function", + "tags": [], + "label": "asKqlFilter", + "description": [], + "signature": [ + "(entityInstance: { entity: Pick<{ id: string; type: string; schema_version: string; identity_fields: string | string[]; display_name: string; definition_version: string; definition_id: string; last_seen_timestamp: string; metrics?: Record | undefined; }, \"identity_fields\">; } & Required) => string" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.asKqlFilter.$1", + "type": "CompoundType", + "tags": [], + "label": "entityInstance", + "description": [], + "signature": [ + "{ entity: Pick<{ id: string; type: string; schema_version: string; identity_fields: string | string[]; display_name: string; definition_version: string; definition_id: string; last_seen_timestamp: string; metrics?: Record | undefined; }, \"identity_fields\">; } & Required" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.getIdentityFieldsValue", + "type": "Function", + "tags": [], + "label": "getIdentityFieldsValue", + "description": [], + "signature": [ + "(entityInstance: { entity: Pick<{ id: string; type: string; schema_version: string; identity_fields: string | string[]; display_name: string; definition_version: string; definition_id: string; last_seen_timestamp: string; metrics?: Record | undefined; }, \"identity_fields\">; } & Required) => Record" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "entityManager", + "id": "def-public.EntityClient.getIdentityFieldsValue.$1", + "type": "CompoundType", + "tags": [], + "label": "entityInstance", + "description": [], + "signature": [ + "{ entity: Pick<{ id: string; type: string; schema_version: string; identity_fields: string | string[]; display_name: string; definition_version: string; definition_id: string; last_seen_timestamp: string; metrics?: Record | undefined; }, \"identity_fields\">; } & Required" + ], + "path": "x-pack/plugins/entity_manager/public/lib/entity_client.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "entityManager", "id": "def-public.EntityManagerUnauthorizedError", @@ -174,7 +1161,13 @@ "label": "entityClient", "description": [], "signature": [ - "EntityClient" + { + "pluginId": "entityManager", + "scope": "public", + "docId": "kibEntityManagerPluginApi", + "section": "def-public.EntityClient", + "text": "EntityClient" + } ], "path": "x-pack/plugins/entity_manager/public/types.ts", "deprecated": false, @@ -203,7 +1196,13 @@ "label": "entityClient", "description": [], "signature": [ - "EntityClient" + { + "pluginId": "entityManager", + "scope": "public", + "docId": "kibEntityManagerPluginApi", + "section": "def-public.EntityClient", + "text": "EntityClient" + } ], "path": "x-pack/plugins/entity_manager/public/types.ts", "deprecated": false, @@ -243,7 +1242,51 @@ "label": "EntityManagerRouteRepository", "description": [], "signature": [ - "{ \"PATCH /internal/entities/definition/{id}\": ", + "{ \"POST /internal/entities/v2/_search/preview\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search/preview\", Zod.ZodObject<{ body: Zod.ZodObject<{ sources: Zod.ZodArray>; index_patterns: Zod.ZodArray; identity_fields: Zod.ZodArray; metadata_fields: Zod.ZodArray; filters: Zod.ZodArray; }, \"strip\", Zod.ZodTypeAny, { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }, { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }>, \"many\">; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }, { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { start: string; end: string; sources: { type: string; filters: string[]; timestamp_field: string; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; }[]; limit: number; }; }, { body: { sources: { type: string; filters: string[]; metadata_fields: string[]; index_patterns: string[]; identity_fields: string[]; timestamp_field?: string | undefined; }[]; start?: string | undefined; end?: string | undefined; limit?: number | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + "<{ entities: ", + { + "pluginId": "@kbn/entities-schema", + "scope": "common", + "docId": "kibKbnEntitiesSchemaPluginApi", + "section": "def-common.EntityV2", + "text": "EntityV2" + }, + "[]; }>, undefined>; \"POST /internal/entities/v2/_search\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/entities/v2/_search\", Zod.ZodObject<{ body: Zod.ZodObject<{ type: Zod.ZodString; metadata_fields: Zod.ZodDefault>>; filters: Zod.ZodDefault>>; start: Zod.ZodEffects>, string, string | undefined>; end: Zod.ZodEffects>, string, string | undefined>; limit: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }, { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { type: string; start: string; end: string; filters: string[]; limit: number; metadata_fields: string[]; }; }, { body: { type: string; start?: string | undefined; end?: string | undefined; filters?: string[] | undefined; limit?: number | undefined; metadata_fields?: string[] | undefined; }; }>, ", + "EntityManagerRouteHandlerResources", + ", ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.IKibanaResponse", + "text": "IKibanaResponse" + }, + ", undefined>; \"PATCH /internal/entities/definition/{id}\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -333,15 +1376,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"POST /internal/entities/definition/{id}/_reset\": ", + ", undefined>; \"POST /internal/entities/definition/{id}/_reset\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -359,15 +1394,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"GET /internal/entities/definition/{id?}\": ", + ", undefined>; \"GET /internal/entities/definition/{id?}\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -385,15 +1412,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"DELETE /internal/entities/definition/{id}\": ", + ", undefined>; \"DELETE /internal/entities/definition/{id}\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -411,15 +1430,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"POST /internal/entities/definition\": ", + ", undefined>; \"POST /internal/entities/definition\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -509,15 +1520,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"DELETE /internal/entities/managed/enablement\": ", + ", undefined>; \"DELETE /internal/entities/managed/enablement\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -535,15 +1538,7 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"PUT /internal/entities/managed/enablement\": ", + ", undefined>; \"PUT /internal/entities/managed/enablement\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -561,25 +1556,9 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"GET /internal/entities/managed/enablement\": ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.ServerRoute", - "text": "ServerRoute" - }, - "<\"GET /internal/entities/managed/enablement\", undefined, ", + ", undefined>; \"GET /internal/entities/managed/enablement\": { endpoint: \"GET /internal/entities/managed/enablement\"; handler: ServerRouteHandler<", "EntityManagerRouteHandlerResources", - ", ", + ", undefined, ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -587,15 +1566,15 @@ "section": "def-server.IKibanaResponse", "text": "IKibanaResponse" }, - ", ", + ">; security?: ", { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" }, - ">; }" + " | undefined; }; }" ], "path": "x-pack/plugins/entity_manager/server/routes/index.ts", "deprecated": false, diff --git a/api_docs/entity_manager.mdx b/api_docs/entity_manager.mdx index d6856ee270d27..11ded400d81ba 100644 --- a/api_docs/entity_manager.mdx +++ b/api_docs/entity_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/entityManager title: "entityManager" image: https://source.unsplash.com/400x175/?github description: API docs for the entityManager plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'entityManager'] --- import entityManagerObj from './entity_manager.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entiti | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 20 | 3 | +| 35 | 0 | 35 | 2 | ## Client diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 71ea62f1d0886..12256929bb526 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/esql.mdx b/api_docs/esql.mdx index 73a8cec2d5d22..75071d6f03369 100644 --- a/api_docs/esql.mdx +++ b/api_docs/esql.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esql title: "esql" image: https://source.unsplash.com/400x175/?github description: API docs for the esql plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esql'] --- import esqlObj from './esql.devdocs.json'; diff --git a/api_docs/esql_data_grid.mdx b/api_docs/esql_data_grid.mdx index e976b72ccbc3c..b6d6fc96cb2a9 100644 --- a/api_docs/esql_data_grid.mdx +++ b/api_docs/esql_data_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esqlDataGrid title: "esqlDataGrid" image: https://source.unsplash.com/400x175/?github description: API docs for the esqlDataGrid plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esqlDataGrid'] --- import esqlDataGridObj from './esql_data_grid.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 6a5e39f98e57e..66adba7501c4c 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_annotation_listing.mdx b/api_docs/event_annotation_listing.mdx index b68acc6aae107..82ff1773c9132 100644 --- a/api_docs/event_annotation_listing.mdx +++ b/api_docs/event_annotation_listing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotationListing title: "eventAnnotationListing" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotationListing plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotationListing'] --- import eventAnnotationListingObj from './event_annotation_listing.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 86306851253b0..2cfb9a1865461 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 54dee6f42f36d..1ebb14f900e7a 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 57e3252c5b9fd..b69ddb7e91eff 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index b6b78728afb0b..383497073288f 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 834736852c129..80e5b8321c25d 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 289e994db73cc..ea30bfd79a70e 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index a58694530b8c6..6481f46f9c1da 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index a1a500a4e34bb..47af3b8bd93ea 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 8c65e20d9a7b5..3bb05c0bb32e8 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 5e6c0d8d9a0b7..81cdc668f7df9 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index 89ae0a046fd6b..ba0c4c3566baa 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 6ff7ee1ef4874..113d89ae93572 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 961c00ac133ed..07ae5d1173a1f 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 3b49f274c7d35..d4f92e3f290e2 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 8f51158e4b64e..fc1a34d457a4f 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 190d68318871f..ce44d06397ca4 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 74e11e54e571c..1136571cf2fbb 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 2016174d824ba..c11d90b1bb6f2 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/fields_metadata.mdx b/api_docs/fields_metadata.mdx index ae6c21e33e75f..866ece3fdf83a 100644 --- a/api_docs/fields_metadata.mdx +++ b/api_docs/fields_metadata.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldsMetadata title: "fieldsMetadata" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldsMetadata plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldsMetadata'] --- import fieldsMetadataObj from './fields_metadata.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 81fa1a1bec2ba..a49cc04b0f062 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 535a33443751b..b132ec182e288 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 30b87f6f2144b..bf58550b38582 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.devdocs.json b/api_docs/fleet.devdocs.json index 4d371badd779c..6af93b912833f 100644 --- a/api_docs/fleet.devdocs.json +++ b/api_docs/fleet.devdocs.json @@ -20134,6 +20134,57 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "fleet", + "id": "def-server.FleetStartContract.createOutputClient", + "type": "Function", + "tags": [], + "label": "createOutputClient", + "description": [ + "\nCreate a Fleet Output Client instance" + ], + "signature": [ + "(request: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => Promise<", + "OutputClientInterface", + ">" + ], + "path": "x-pack/plugins/fleet/server/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "fleet", + "id": "def-server.FleetStartContract.createOutputClient.$1", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "x-pack/plugins/fleet/server/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] } ], "lifecycle": "start", diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 4b15fbe5e9605..a2dd317945c53 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) for questi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1428 | 5 | 1303 | 81 | +| 1430 | 5 | 1304 | 82 | ## Client diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 93b2911bb9dac..684d940f8a9d6 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index 6bdbe7638cc50..761c1e9125300 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index aaecc50110ff9..144234325c96a 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index cfdd524b8c4b9..dd6def7a1cf29 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 4989a78d7a72f..b05f09e3f517d 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 354ee844c309f..a8cf21f9398e1 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/inference.mdx b/api_docs/inference.mdx index 93db807612e58..7d499e10dfc25 100644 --- a/api_docs/inference.mdx +++ b/api_docs/inference.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inference title: "inference" image: https://source.unsplash.com/400x175/?github description: API docs for the inference plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inference'] --- import inferenceObj from './inference.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 54a886e8d430d..3c8931f37c186 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/ingest_pipelines.mdx b/api_docs/ingest_pipelines.mdx index a6864f177fb9f..f75d0dd39ca3e 100644 --- a/api_docs/ingest_pipelines.mdx +++ b/api_docs/ingest_pipelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ingestPipelines title: "ingestPipelines" image: https://source.unsplash.com/400x175/?github description: API docs for the ingestPipelines plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ingestPipelines'] --- import ingestPipelinesObj from './ingest_pipelines.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 695ec48139c30..327ee9d4b9458 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/integration_assistant.mdx b/api_docs/integration_assistant.mdx index 7d6cae5abf1bc..43128fcd76713 100644 --- a/api_docs/integration_assistant.mdx +++ b/api_docs/integration_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/integrationAssistant title: "integrationAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the integrationAssistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'integrationAssistant'] --- import integrationAssistantObj from './integration_assistant.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 05468989ddaab..0acb84296319f 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/inventory.mdx b/api_docs/inventory.mdx index aa17f23939619..be53736459451 100644 --- a/api_docs/inventory.mdx +++ b/api_docs/inventory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inventory title: "inventory" image: https://source.unsplash.com/400x175/?github description: API docs for the inventory plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inventory'] --- import inventoryObj from './inventory.devdocs.json'; diff --git a/api_docs/investigate.mdx b/api_docs/investigate.mdx index 3520f542e229b..1da148497ed60 100644 --- a/api_docs/investigate.mdx +++ b/api_docs/investigate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/investigate title: "investigate" image: https://source.unsplash.com/400x175/?github description: API docs for the investigate plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'investigate'] --- import investigateObj from './investigate.devdocs.json'; diff --git a/api_docs/investigate_app.mdx b/api_docs/investigate_app.mdx index b4d0a80508452..8e368221f13ca 100644 --- a/api_docs/investigate_app.mdx +++ b/api_docs/investigate_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/investigateApp title: "investigateApp" image: https://source.unsplash.com/400x175/?github description: API docs for the investigateApp plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'investigateApp'] --- import investigateAppObj from './investigate_app.devdocs.json'; diff --git a/api_docs/kbn_actions_types.mdx b/api_docs/kbn_actions_types.mdx index 3f9fee6b535c3..0976b60a54583 100644 --- a/api_docs/kbn_actions_types.mdx +++ b/api_docs/kbn_actions_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-actions-types title: "@kbn/actions-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/actions-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/actions-types'] --- import kbnActionsTypesObj from './kbn_actions_types.devdocs.json'; diff --git a/api_docs/kbn_ai_assistant.mdx b/api_docs/kbn_ai_assistant.mdx index 254f5dd2b38e4..8128e9639bc72 100644 --- a/api_docs/kbn_ai_assistant.mdx +++ b/api_docs/kbn_ai_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ai-assistant title: "@kbn/ai-assistant" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ai-assistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ai-assistant'] --- import kbnAiAssistantObj from './kbn_ai_assistant.devdocs.json'; diff --git a/api_docs/kbn_ai_assistant_common.mdx b/api_docs/kbn_ai_assistant_common.mdx index 3b95d229a5960..898d648ba9a28 100644 --- a/api_docs/kbn_ai_assistant_common.mdx +++ b/api_docs/kbn_ai_assistant_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ai-assistant-common title: "@kbn/ai-assistant-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ai-assistant-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ai-assistant-common'] --- import kbnAiAssistantCommonObj from './kbn_ai_assistant_common.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 2fc68aaca35ce..b21fb6d1244ec 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_log_pattern_analysis.mdx b/api_docs/kbn_aiops_log_pattern_analysis.mdx index a6815681ce673..488f33b5013a6 100644 --- a/api_docs/kbn_aiops_log_pattern_analysis.mdx +++ b/api_docs/kbn_aiops_log_pattern_analysis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-log-pattern-analysis title: "@kbn/aiops-log-pattern-analysis" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-log-pattern-analysis plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-log-pattern-analysis'] --- import kbnAiopsLogPatternAnalysisObj from './kbn_aiops_log_pattern_analysis.devdocs.json'; diff --git a/api_docs/kbn_aiops_log_rate_analysis.mdx b/api_docs/kbn_aiops_log_rate_analysis.mdx index d3adfe40323ef..a7a1a4fb3877f 100644 --- a/api_docs/kbn_aiops_log_rate_analysis.mdx +++ b/api_docs/kbn_aiops_log_rate_analysis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-log-rate-analysis title: "@kbn/aiops-log-rate-analysis" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-log-rate-analysis plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-log-rate-analysis'] --- import kbnAiopsLogRateAnalysisObj from './kbn_aiops_log_rate_analysis.devdocs.json'; diff --git a/api_docs/kbn_alerting_api_integration_helpers.mdx b/api_docs/kbn_alerting_api_integration_helpers.mdx index ec363b57d1618..6b9f8bdad7912 100644 --- a/api_docs/kbn_alerting_api_integration_helpers.mdx +++ b/api_docs/kbn_alerting_api_integration_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-api-integration-helpers title: "@kbn/alerting-api-integration-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-api-integration-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-api-integration-helpers'] --- import kbnAlertingApiIntegrationHelpersObj from './kbn_alerting_api_integration_helpers.devdocs.json'; diff --git a/api_docs/kbn_alerting_comparators.mdx b/api_docs/kbn_alerting_comparators.mdx index df6278d741128..23b2ce62abda5 100644 --- a/api_docs/kbn_alerting_comparators.mdx +++ b/api_docs/kbn_alerting_comparators.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-comparators title: "@kbn/alerting-comparators" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-comparators plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-comparators'] --- import kbnAlertingComparatorsObj from './kbn_alerting_comparators.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 9e142b96ba8ca..c1fdcf7de1dc5 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerting_types.mdx b/api_docs/kbn_alerting_types.mdx index 3899a44b8a457..be586d0ae4ffb 100644 --- a/api_docs/kbn_alerting_types.mdx +++ b/api_docs/kbn_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-types title: "@kbn/alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-types'] --- import kbnAlertingTypesObj from './kbn_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index 2c43f24595afb..f9e83c23945b2 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts_grouping.mdx b/api_docs/kbn_alerts_grouping.mdx index c88ae5ad61830..63bc41eab7198 100644 --- a/api_docs/kbn_alerts_grouping.mdx +++ b/api_docs/kbn_alerts_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-grouping title: "@kbn/alerts-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-grouping plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-grouping'] --- import kbnAlertsGroupingObj from './kbn_alerts_grouping.devdocs.json'; diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index 5f15133af906d..e27bbc7f352fb 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 54eb2a99f9ffe..493899de7549b 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_collection_utils.mdx b/api_docs/kbn_analytics_collection_utils.mdx index b938716b85051..6f18b63dec731 100644 --- a/api_docs/kbn_analytics_collection_utils.mdx +++ b/api_docs/kbn_analytics_collection_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-collection-utils title: "@kbn/analytics-collection-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-collection-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-collection-utils'] --- import kbnAnalyticsCollectionUtilsObj from './kbn_analytics_collection_utils.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 45c6d171b9d5d..4478066f05135 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_data_view.mdx b/api_docs/kbn_apm_data_view.mdx index 945ef3fe2aa4e..a8f55d00181f1 100644 --- a/api_docs/kbn_apm_data_view.mdx +++ b/api_docs/kbn_apm_data_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-data-view title: "@kbn/apm-data-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-data-view plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-data-view'] --- import kbnApmDataViewObj from './kbn_apm_data_view.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 5ef2fdf98029a..c711daf49a1e5 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.devdocs.json b/api_docs/kbn_apm_synthtrace_client.devdocs.json index 4f458c6eb82a9..5aa9b2ee2e090 100644 --- a/api_docs/kbn_apm_synthtrace_client.devdocs.json +++ b/api_docs/kbn_apm_synthtrace_client.devdocs.json @@ -3133,7 +3133,7 @@ "label": "LogDocument", "description": [], "signature": [ - "{ '@timestamp'?: number | undefined; } & Partial<{ 'input.type': string; 'log.file.path'?: string | undefined; 'service.name'?: string | undefined; 'service.environment'?: string | undefined; 'data_stream.namespace': string; 'data_stream.type': string; 'data_stream.dataset': string; message?: string | undefined; 'error.message'?: string | undefined; 'event.original'?: string | undefined; 'event.dataset': string; 'log.level'?: string | undefined; 'host.name'?: string | undefined; 'container.id'?: string | undefined; 'trace.id'?: string | undefined; 'transaction.id'?: string | undefined; 'agent.id'?: string | undefined; 'agent.name'?: string | undefined; 'orchestrator.cluster.name'?: string | undefined; 'orchestrator.cluster.id'?: string | undefined; 'orchestrator.resource.id'?: string | undefined; 'kubernetes.pod.uid'?: string | undefined; 'aws.s3.bucket.name'?: string | undefined; 'aws.kinesis.name'?: string | undefined; 'orchestrator.namespace'?: string | undefined; 'container.name'?: string | undefined; 'cloud.provider'?: string | undefined; 'cloud.region'?: string | undefined; 'cloud.availability_zone'?: string | undefined; 'cloud.project.id'?: string | undefined; 'cloud.instance.id'?: string | undefined; 'error.stack_trace'?: string | undefined; 'error.exception.stacktrace'?: string | undefined; 'error.log.stacktrace'?: string | undefined; 'log.custom': Record; 'host.geo.location': number[]; 'host.ip': string; 'network.bytes': number; 'tls.established': boolean; 'event.duration': number; 'event.start': Date; 'event.end': Date; labels?: Record | undefined; test_field: string | string[]; date: Date; severity: string; msg: string; svc: string; hostname: string; thisisaverylongfieldnamethatevendoesnotcontainanyspaceswhyitcouldpotentiallybreakouruiinseveralplaces: string; }>" + "{ '@timestamp'?: number | undefined; } & Partial<{ _index?: string | undefined; 'input.type': string; 'log.file.path'?: string | undefined; 'service.name'?: string | undefined; 'service.environment'?: string | undefined; 'data_stream.namespace': string; 'data_stream.type': string; 'data_stream.dataset': string; message?: string | undefined; 'error.message'?: string | undefined; 'event.original'?: string | undefined; 'event.dataset': string; 'log.level'?: string | undefined; 'host.name'?: string | undefined; 'container.id'?: string | undefined; 'trace.id'?: string | undefined; 'transaction.id'?: string | undefined; 'agent.id'?: string | undefined; 'agent.name'?: string | undefined; 'orchestrator.cluster.name'?: string | undefined; 'orchestrator.cluster.id'?: string | undefined; 'orchestrator.resource.id'?: string | undefined; 'kubernetes.pod.uid'?: string | undefined; 'aws.s3.bucket.name'?: string | undefined; 'aws.kinesis.name'?: string | undefined; 'orchestrator.namespace'?: string | undefined; 'container.name'?: string | undefined; 'cloud.provider'?: string | undefined; 'cloud.region'?: string | undefined; 'cloud.availability_zone'?: string | undefined; 'cloud.project.id'?: string | undefined; 'cloud.instance.id'?: string | undefined; 'error.stack_trace'?: string | undefined; 'error.exception.stacktrace'?: string | undefined; 'error.log.stacktrace'?: string | undefined; 'log.custom': Record; 'host.geo.location': number[]; 'host.ip': string; 'network.bytes': number; 'tls.established': boolean; 'event.duration': number; 'event.start': Date; 'event.end': Date; labels?: Record | undefined; test_field: string | string[]; date: Date; severity: string; msg: string; svc: string; hostname: string; 'http.status_code'?: number | undefined; 'http.request.method'?: string | undefined; 'url.path'?: string | undefined; 'process.name'?: string | undefined; 'kubernetes.namespace'?: string | undefined; 'kubernetes.pod.name'?: string | undefined; 'kubernetes.container.name'?: string | undefined; 'orchestrator.resource.name'?: string | undefined; thisisaverylongfieldnamethatevendoesnotcontainanyspaceswhyitcouldpotentiallybreakouruiinseveralplaces: string; }>" ], "path": "packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts", "deprecated": false, @@ -4167,6 +4167,34 @@ } ] }, + { + "parentPluginId": "@kbn/apm-synthtrace-client", + "id": "def-common.log.createForIndex", + "type": "Function", + "tags": [], + "label": "createForIndex", + "description": [], + "signature": [ + "(index: string) => Log" + ], + "path": "packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/apm-synthtrace-client", + "id": "def-common.log.createForIndex.$1", + "type": "string", + "tags": [], + "label": "index", + "description": [], + "path": "packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, { "parentPluginId": "@kbn/apm-synthtrace-client", "id": "def-common.log.createMinimal", diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index 313228821e9cf..f3c18c109873a 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 268 | 0 | 268 | 38 | +| 270 | 0 | 270 | 38 | ## Common diff --git a/api_docs/kbn_apm_types.mdx b/api_docs/kbn_apm_types.mdx index c63d8466e3e56..142589a1c726d 100644 --- a/api_docs/kbn_apm_types.mdx +++ b/api_docs/kbn_apm_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-types title: "@kbn/apm-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-types'] --- import kbnApmTypesObj from './kbn_apm_types.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index a05d2191c0882..9561df4fa7811 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_avc_banner.mdx b/api_docs/kbn_avc_banner.mdx index d5be6cc3f8020..7f90f67755e6e 100644 --- a/api_docs/kbn_avc_banner.mdx +++ b/api_docs/kbn_avc_banner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-avc-banner title: "@kbn/avc-banner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/avc-banner plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/avc-banner'] --- import kbnAvcBannerObj from './kbn_avc_banner.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index d6441c0935d13..5e184651daec6 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_bfetch_error.mdx b/api_docs/kbn_bfetch_error.mdx index 6bac3c36f2e32..e40bd88901e95 100644 --- a/api_docs/kbn_bfetch_error.mdx +++ b/api_docs/kbn_bfetch_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-bfetch-error title: "@kbn/bfetch-error" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/bfetch-error plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bfetch-error'] --- import kbnBfetchErrorObj from './kbn_bfetch_error.devdocs.json'; diff --git a/api_docs/kbn_calculate_auto.mdx b/api_docs/kbn_calculate_auto.mdx index 378767a0a261d..4ea64480de7da 100644 --- a/api_docs/kbn_calculate_auto.mdx +++ b/api_docs/kbn_calculate_auto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-calculate-auto title: "@kbn/calculate-auto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/calculate-auto plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/calculate-auto'] --- import kbnCalculateAutoObj from './kbn_calculate_auto.devdocs.json'; diff --git a/api_docs/kbn_calculate_width_from_char_count.mdx b/api_docs/kbn_calculate_width_from_char_count.mdx index 5e5de0c61abd3..78f92d67a3a10 100644 --- a/api_docs/kbn_calculate_width_from_char_count.mdx +++ b/api_docs/kbn_calculate_width_from_char_count.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-calculate-width-from-char-count title: "@kbn/calculate-width-from-char-count" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/calculate-width-from-char-count plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/calculate-width-from-char-count'] --- import kbnCalculateWidthFromCharCountObj from './kbn_calculate_width_from_char_count.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 537ffa21830b3..b3d762f7f740d 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cbor.mdx b/api_docs/kbn_cbor.mdx index 6c60239b17d16..5221c426632b4 100644 --- a/api_docs/kbn_cbor.mdx +++ b/api_docs/kbn_cbor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cbor title: "@kbn/cbor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cbor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cbor'] --- import kbnCborObj from './kbn_cbor.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index 25c7ed965d45b..6e866f3409dad 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index bdc7fc4420281..c5b22948264b2 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index 5ced1454b662b..eca2073a78051 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 009d09a0a930d..78ff8ac4bb780 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 403fcf05888c9..d2a4c39dc3d21 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 6d1b326868d92..b1afb22526d68 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 7f3afbd008686..15aa4c0da0c91 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_cloud_security_posture.mdx b/api_docs/kbn_cloud_security_posture.mdx index 040ddbb32d130..29b13d0d8d621 100644 --- a/api_docs/kbn_cloud_security_posture.mdx +++ b/api_docs/kbn_cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cloud-security-posture title: "@kbn/cloud-security-posture" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cloud-security-posture plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cloud-security-posture'] --- import kbnCloudSecurityPostureObj from './kbn_cloud_security_posture.devdocs.json'; diff --git a/api_docs/kbn_cloud_security_posture_common.mdx b/api_docs/kbn_cloud_security_posture_common.mdx index 8a25054cc686f..f542be543cf06 100644 --- a/api_docs/kbn_cloud_security_posture_common.mdx +++ b/api_docs/kbn_cloud_security_posture_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cloud-security-posture-common title: "@kbn/cloud-security-posture-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cloud-security-posture-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cloud-security-posture-common'] --- import kbnCloudSecurityPostureCommonObj from './kbn_cloud_security_posture_common.devdocs.json'; diff --git a/api_docs/kbn_cloud_security_posture_graph.mdx b/api_docs/kbn_cloud_security_posture_graph.mdx index 877ed9e162eff..3c4d5b472e4ec 100644 --- a/api_docs/kbn_cloud_security_posture_graph.mdx +++ b/api_docs/kbn_cloud_security_posture_graph.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cloud-security-posture-graph title: "@kbn/cloud-security-posture-graph" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cloud-security-posture-graph plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cloud-security-posture-graph'] --- import kbnCloudSecurityPostureGraphObj from './kbn_cloud_security_posture_graph.devdocs.json'; diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 3e40426381ea6..0f3d0be44c55b 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mock.mdx b/api_docs/kbn_code_editor_mock.mdx index 09d5418473f94..33ae03805b61a 100644 --- a/api_docs/kbn_code_editor_mock.mdx +++ b/api_docs/kbn_code_editor_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mock title: "@kbn/code-editor-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mock plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mock'] --- import kbnCodeEditorMockObj from './kbn_code_editor_mock.devdocs.json'; diff --git a/api_docs/kbn_code_owners.mdx b/api_docs/kbn_code_owners.mdx index 286833fc3fa61..61b8f2569c39f 100644 --- a/api_docs/kbn_code_owners.mdx +++ b/api_docs/kbn_code_owners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-owners title: "@kbn/code-owners" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-owners plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-owners'] --- import kbnCodeOwnersObj from './kbn_code_owners.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index dc24d179f72cd..05b51dec1aa63 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index e5408cb5acde9..f2a5b134aa13a 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index e3dadc72f923b..56e0b59367bed 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 40b415251d9ce..92b4ae88d8690 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 0e9d4232ecee6..faf30dc214ea1 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_insights_public.mdx b/api_docs/kbn_content_management_content_insights_public.mdx index c44b78488217e..a665b0db08ec0 100644 --- a/api_docs/kbn_content_management_content_insights_public.mdx +++ b/api_docs/kbn_content_management_content_insights_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-insights-public title: "@kbn/content-management-content-insights-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-insights-public plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-insights-public'] --- import kbnContentManagementContentInsightsPublicObj from './kbn_content_management_content_insights_public.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_insights_server.mdx b/api_docs/kbn_content_management_content_insights_server.mdx index 0fb1a295d1563..00ce49ef6ad9a 100644 --- a/api_docs/kbn_content_management_content_insights_server.mdx +++ b/api_docs/kbn_content_management_content_insights_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-insights-server title: "@kbn/content-management-content-insights-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-insights-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-insights-server'] --- import kbnContentManagementContentInsightsServerObj from './kbn_content_management_content_insights_server.devdocs.json'; diff --git a/api_docs/kbn_content_management_favorites_common.mdx b/api_docs/kbn_content_management_favorites_common.mdx index 9b902ca158480..2401c3a9eed87 100644 --- a/api_docs/kbn_content_management_favorites_common.mdx +++ b/api_docs/kbn_content_management_favorites_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-favorites-common title: "@kbn/content-management-favorites-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-favorites-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-favorites-common'] --- import kbnContentManagementFavoritesCommonObj from './kbn_content_management_favorites_common.devdocs.json'; diff --git a/api_docs/kbn_content_management_favorites_public.mdx b/api_docs/kbn_content_management_favorites_public.mdx index 7eb75aa665044..7cd275df15c82 100644 --- a/api_docs/kbn_content_management_favorites_public.mdx +++ b/api_docs/kbn_content_management_favorites_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-favorites-public title: "@kbn/content-management-favorites-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-favorites-public plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-favorites-public'] --- import kbnContentManagementFavoritesPublicObj from './kbn_content_management_favorites_public.devdocs.json'; diff --git a/api_docs/kbn_content_management_favorites_server.mdx b/api_docs/kbn_content_management_favorites_server.mdx index 5c172cd208365..a7b45b1118add 100644 --- a/api_docs/kbn_content_management_favorites_server.mdx +++ b/api_docs/kbn_content_management_favorites_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-favorites-server title: "@kbn/content-management-favorites-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-favorites-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-favorites-server'] --- import kbnContentManagementFavoritesServerObj from './kbn_content_management_favorites_server.devdocs.json'; diff --git a/api_docs/kbn_content_management_tabbed_table_list_view.mdx b/api_docs/kbn_content_management_tabbed_table_list_view.mdx index 2ed330e108390..d456cb698a011 100644 --- a/api_docs/kbn_content_management_tabbed_table_list_view.mdx +++ b/api_docs/kbn_content_management_tabbed_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-tabbed-table-list-view title: "@kbn/content-management-tabbed-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-tabbed-table-list-view plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-tabbed-table-list-view'] --- import kbnContentManagementTabbedTableListViewObj from './kbn_content_management_tabbed_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view.mdx b/api_docs/kbn_content_management_table_list_view.mdx index eef9d63222bbd..88bf9b727d3e8 100644 --- a/api_docs/kbn_content_management_table_list_view.mdx +++ b/api_docs/kbn_content_management_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view title: "@kbn/content-management-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view'] --- import kbnContentManagementTableListViewObj from './kbn_content_management_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view_common.mdx b/api_docs/kbn_content_management_table_list_view_common.mdx index e29d9a67be0ef..ef7fa599c1fc1 100644 --- a/api_docs/kbn_content_management_table_list_view_common.mdx +++ b/api_docs/kbn_content_management_table_list_view_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view-common title: "@kbn/content-management-table-list-view-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-common'] --- import kbnContentManagementTableListViewCommonObj from './kbn_content_management_table_list_view_common.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view_table.mdx b/api_docs/kbn_content_management_table_list_view_table.mdx index 0c275a5216b3f..a03b070a94afc 100644 --- a/api_docs/kbn_content_management_table_list_view_table.mdx +++ b/api_docs/kbn_content_management_table_list_view_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view-table title: "@kbn/content-management-table-list-view-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view-table plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-table'] --- import kbnContentManagementTableListViewTableObj from './kbn_content_management_table_list_view_table.devdocs.json'; diff --git a/api_docs/kbn_content_management_user_profiles.mdx b/api_docs/kbn_content_management_user_profiles.mdx index 5c7d6109f24bd..eb76aa26d8544 100644 --- a/api_docs/kbn_content_management_user_profiles.mdx +++ b/api_docs/kbn_content_management_user_profiles.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-user-profiles title: "@kbn/content-management-user-profiles" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-user-profiles plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-user-profiles'] --- import kbnContentManagementUserProfilesObj from './kbn_content_management_user_profiles.devdocs.json'; diff --git a/api_docs/kbn_content_management_utils.mdx b/api_docs/kbn_content_management_utils.mdx index 6300345f4913d..15e0ea70c321e 100644 --- a/api_docs/kbn_content_management_utils.mdx +++ b/api_docs/kbn_content_management_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-utils title: "@kbn/content-management-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-utils'] --- import kbnContentManagementUtilsObj from './kbn_content_management_utils.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 8066df24b148f..267d867411fd6 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index c4ab538354e0d..b352c238d7bd2 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 787f96a0c690d..0fbce3beb1916 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 64f8a813599e3..38bf70ad35046 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 8fd61324c6386..9c2d9d85b6a6c 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index 4e81c9fe41047..7118db9267cac 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 8bdf56b2ba972..0c5ca2917ab60 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 6391b315d4e06..10a2c44231c45 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index a96be3b8182ba..1f904828c895d 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 8b333782ee6bb..65edf8694b51f 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 4b2145272c5a4..d4666008af63f 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index f18dc0bf8820e..ba029252262c4 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 20d7fb801dfb7..bdda7cc75c86c 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 38a1558816956..30a6893edfec0 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 7e595442d34a1..68b2f28c99568 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 1b016542fa810..0df60c14545e8 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 9445c455a8ef9..7f1444be35c48 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index dca19153744f4..b97dcc9c375c2 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index f4c0f99be42c2..41a74ff222358 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 581e17975192b..24707882c708b 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 6a1da1b91387d..c7dd4f86977b1 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.devdocs.json b/api_docs/kbn_core_chrome_browser.devdocs.json index 31736959b7379..acc96d2631c68 100644 --- a/api_docs/kbn_core_chrome_browser.devdocs.json +++ b/api_docs/kbn_core_chrome_browser.devdocs.json @@ -3765,7 +3765,7 @@ "label": "AppDeepLinkId", "description": [], "signature": [ - "\"fleet\" | \"graph\" | \"ml\" | \"monitoring\" | \"profiling\" | \"metrics\" | \"management\" | \"apm\" | \"synthetics\" | \"ux\" | \"canvas\" | \"logs\" | \"dashboards\" | \"slo\" | \"observabilityAIAssistant\" | \"home\" | \"integrations\" | \"discover\" | \"observability-overview\" | \"appSearch\" | \"dev_tools\" | \"maps\" | \"visualize\" | \"dev_tools:console\" | \"dev_tools:searchprofiler\" | \"dev_tools:painless_lab\" | \"dev_tools:grokdebugger\" | \"ml:notifications\" | \"ml:nodes\" | \"ml:overview\" | \"ml:memoryUsage\" | \"ml:settings\" | \"ml:dataVisualizer\" | \"ml:logPatternAnalysis\" | \"ml:logRateAnalysis\" | \"ml:singleMetricViewer\" | \"ml:anomalyDetection\" | \"ml:anomalyExplorer\" | \"ml:dataDrift\" | \"ml:dataFrameAnalytics\" | \"ml:resultExplorer\" | \"ml:analyticsMap\" | \"ml:aiOps\" | \"ml:changePointDetections\" | \"ml:modelManagement\" | \"ml:nodesOverview\" | \"ml:esqlDataVisualizer\" | \"ml:fileUpload\" | \"ml:indexDataVisualizer\" | \"ml:calendarSettings\" | \"ml:filterListsSettings\" | \"ml:suppliedConfigurations\" | \"osquery\" | \"management:transform\" | \"management:watcher\" | \"management:cases\" | \"management:tags\" | \"management:maintenanceWindows\" | \"management:cross_cluster_replication\" | \"management:dataViews\" | \"management:spaces\" | \"management:settings\" | \"management:users\" | \"management:migrate_data\" | \"management:search_sessions\" | \"management:data_quality\" | \"management:filesManagement\" | \"management:roles\" | \"management:reporting\" | \"management:aiAssistantManagementSelection\" | \"management:securityAiAssistantManagement\" | \"management:observabilityAiAssistantManagement\" | \"management:api_keys\" | \"management:license_management\" | \"management:index_lifecycle_management\" | \"management:index_management\" | \"management:ingest_pipelines\" | \"management:jobsListLink\" | \"management:objects\" | \"management:pipelines\" | \"management:remote_clusters\" | \"management:role_mappings\" | \"management:rollup_jobs\" | \"management:snapshot_restore\" | \"management:triggersActions\" | \"management:triggersActionsConnectors\" | \"management:upgrade_assistant\" | \"enterpriseSearch\" | \"enterpriseSearchContent\" | \"enterpriseSearchApplications\" | \"searchInferenceEndpoints\" | \"enterpriseSearchAnalytics\" | \"workplaceSearch\" | \"serverlessElasticsearch\" | \"serverlessConnectors\" | \"serverlessWebCrawlers\" | \"searchPlayground\" | \"searchHomepage\" | \"enterpriseSearchContent:connectors\" | \"enterpriseSearchContent:searchIndices\" | \"enterpriseSearchContent:webCrawlers\" | \"enterpriseSearchApplications:searchApplications\" | \"enterpriseSearchApplications:playground\" | \"appSearch:engines\" | \"searchInferenceEndpoints:inferenceEndpoints\" | \"elasticsearchStart\" | \"elasticsearchIndices\" | \"enterpriseSearchElasticsearch\" | \"enterpriseSearchVectorSearch\" | \"enterpriseSearchSemanticSearch\" | \"enterpriseSearchAISearch\" | \"elasticsearchIndices:createIndex\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\" | \"fleet:settings\" | \"fleet:agents\" | \"fleet:policies\" | \"fleet:data_streams\" | \"fleet:enrollment_tokens\" | \"fleet:uninstall_tokens\"" + "\"fleet\" | \"graph\" | \"ml\" | \"monitoring\" | \"profiling\" | \"metrics\" | \"management\" | \"apm\" | \"synthetics\" | \"ux\" | \"canvas\" | \"logs\" | \"dashboards\" | \"slo\" | \"observabilityAIAssistant\" | \"home\" | \"integrations\" | \"discover\" | \"observability-overview\" | \"streams\" | \"appSearch\" | \"dev_tools\" | \"maps\" | \"visualize\" | \"dev_tools:console\" | \"dev_tools:searchprofiler\" | \"dev_tools:painless_lab\" | \"dev_tools:grokdebugger\" | \"ml:notifications\" | \"ml:nodes\" | \"ml:overview\" | \"ml:memoryUsage\" | \"ml:settings\" | \"ml:dataVisualizer\" | \"ml:logPatternAnalysis\" | \"ml:logRateAnalysis\" | \"ml:singleMetricViewer\" | \"ml:anomalyDetection\" | \"ml:anomalyExplorer\" | \"ml:dataDrift\" | \"ml:dataFrameAnalytics\" | \"ml:resultExplorer\" | \"ml:analyticsMap\" | \"ml:aiOps\" | \"ml:changePointDetections\" | \"ml:modelManagement\" | \"ml:nodesOverview\" | \"ml:esqlDataVisualizer\" | \"ml:fileUpload\" | \"ml:indexDataVisualizer\" | \"ml:calendarSettings\" | \"ml:filterListsSettings\" | \"ml:suppliedConfigurations\" | \"osquery\" | \"management:transform\" | \"management:watcher\" | \"management:cases\" | \"management:tags\" | \"management:maintenanceWindows\" | \"management:cross_cluster_replication\" | \"management:dataViews\" | \"management:spaces\" | \"management:settings\" | \"management:users\" | \"management:migrate_data\" | \"management:search_sessions\" | \"management:data_quality\" | \"management:filesManagement\" | \"management:roles\" | \"management:reporting\" | \"management:aiAssistantManagementSelection\" | \"management:securityAiAssistantManagement\" | \"management:observabilityAiAssistantManagement\" | \"management:api_keys\" | \"management:license_management\" | \"management:index_lifecycle_management\" | \"management:index_management\" | \"management:ingest_pipelines\" | \"management:jobsListLink\" | \"management:objects\" | \"management:pipelines\" | \"management:remote_clusters\" | \"management:role_mappings\" | \"management:rollup_jobs\" | \"management:snapshot_restore\" | \"management:triggersActions\" | \"management:triggersActionsConnectors\" | \"management:upgrade_assistant\" | \"enterpriseSearch\" | \"enterpriseSearchContent\" | \"enterpriseSearchApplications\" | \"searchInferenceEndpoints\" | \"enterpriseSearchAnalytics\" | \"workplaceSearch\" | \"serverlessElasticsearch\" | \"serverlessConnectors\" | \"serverlessWebCrawlers\" | \"searchPlayground\" | \"searchHomepage\" | \"enterpriseSearchContent:connectors\" | \"enterpriseSearchContent:searchIndices\" | \"enterpriseSearchContent:webCrawlers\" | \"enterpriseSearchApplications:searchApplications\" | \"enterpriseSearchApplications:playground\" | \"appSearch:engines\" | \"searchInferenceEndpoints:inferenceEndpoints\" | \"elasticsearchStart\" | \"elasticsearchIndices\" | \"enterpriseSearchElasticsearch\" | \"enterpriseSearchVectorSearch\" | \"enterpriseSearchSemanticSearch\" | \"enterpriseSearchAISearch\" | \"elasticsearchIndices:createIndex\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"streams:overview\" | \"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:siem_migrations-rules\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\" | \"fleet:settings\" | \"fleet:agents\" | \"fleet:policies\" | \"fleet:data_streams\" | \"fleet:enrollment_tokens\" | \"fleet:uninstall_tokens\"" ], "path": "packages/core/chrome/core-chrome-browser/src/project_navigation.ts", "deprecated": false, diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 60c336387a833..fa7a8a730abda 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index 64bfefe8200fc..bb08e7f0eda10 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 797ced21bcbbf..32b03b52c9bbe 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 12a629a41e836..3e3b48ddedc08 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index 906b5decbe13b..24be7286254c0 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 4eb19bfa07056..111cac56cacc7 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index b09d7943d5e5c..6dd2817d6374a 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index 65eb63f52cbdb..b715a4d982d52 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index 8778fa1bc6bf1..3dda0b58ca347 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index ef7dfa5b8775e..c1e766b0727ee 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index 735de0b33963c..e02b162eeca9e 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 18ffa846a324a..f3c0c714f7e89 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 625b11d576d13..a97b31689f76e 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 7b9ab629c8fbc..b81a1e4e62b02 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 1ab77326a814f..611fbd7a8cb7c 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index c912fe5b5d624..b6f0f5d4fcf79 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 751a323909db6..25e8f688c005f 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 6a57d0519db6c..104ea341c319d 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 34b77855ba912..53b3b839bd218 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 0fdc6f9719ea8..4966775844453 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 0855223176737..d5cef304bdfe5 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index c4714af61deb6..cbd6cb06bb6a3 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index d766c3daecf7e..ee42aa49459ea 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index 27fcdd39a6bed..3d6201efba210 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 023dac9acf341..79f41d1118a0b 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 004e1275a4fc8..7af806feed6b3 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index 199363dcfdb9a..edc8af60646ca 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index a62a424f303a9..7fd2356dbd868 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index fc14dc5b6143a..9512ae2112ef9 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index 167d120d06f7d..62d94a3457f87 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 0057708b10e62..54e8db8790974 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 585737e1770e1..bcfe3d579e30d 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 60205be760d12..df26399174491 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 1b069ef1c955f..a76c0b3b7123d 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 309c1a3e11dc8..c0e457ff10f3a 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 2c1089adcc9e1..411a43b275fe8 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 9c0a53358e054..3384e5544bd4e 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_browser.mdx b/api_docs/kbn_core_feature_flags_browser.mdx index 7368b89dd8298..7ce942930de06 100644 --- a/api_docs/kbn_core_feature_flags_browser.mdx +++ b/api_docs/kbn_core_feature_flags_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-browser title: "@kbn/core-feature-flags-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-browser'] --- import kbnCoreFeatureFlagsBrowserObj from './kbn_core_feature_flags_browser.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_browser_internal.mdx b/api_docs/kbn_core_feature_flags_browser_internal.mdx index 7c97ea63d7a35..76cb37d555581 100644 --- a/api_docs/kbn_core_feature_flags_browser_internal.mdx +++ b/api_docs/kbn_core_feature_flags_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-browser-internal title: "@kbn/core-feature-flags-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-browser-internal'] --- import kbnCoreFeatureFlagsBrowserInternalObj from './kbn_core_feature_flags_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_browser_mocks.mdx b/api_docs/kbn_core_feature_flags_browser_mocks.mdx index a5ff3696e75be..2bc580208430e 100644 --- a/api_docs/kbn_core_feature_flags_browser_mocks.mdx +++ b/api_docs/kbn_core_feature_flags_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-browser-mocks title: "@kbn/core-feature-flags-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-browser-mocks'] --- import kbnCoreFeatureFlagsBrowserMocksObj from './kbn_core_feature_flags_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_server.mdx b/api_docs/kbn_core_feature_flags_server.mdx index 9df543abcaaff..103fbf14a6fea 100644 --- a/api_docs/kbn_core_feature_flags_server.mdx +++ b/api_docs/kbn_core_feature_flags_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-server title: "@kbn/core-feature-flags-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-server'] --- import kbnCoreFeatureFlagsServerObj from './kbn_core_feature_flags_server.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_server_internal.mdx b/api_docs/kbn_core_feature_flags_server_internal.mdx index 03a42bc8e1e14..143f2d75eda6d 100644 --- a/api_docs/kbn_core_feature_flags_server_internal.mdx +++ b/api_docs/kbn_core_feature_flags_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-server-internal title: "@kbn/core-feature-flags-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-server-internal'] --- import kbnCoreFeatureFlagsServerInternalObj from './kbn_core_feature_flags_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_feature_flags_server_mocks.mdx b/api_docs/kbn_core_feature_flags_server_mocks.mdx index 11b0f6da49b29..592be7bdddec3 100644 --- a/api_docs/kbn_core_feature_flags_server_mocks.mdx +++ b/api_docs/kbn_core_feature_flags_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-feature-flags-server-mocks title: "@kbn/core-feature-flags-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-feature-flags-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-feature-flags-server-mocks'] --- import kbnCoreFeatureFlagsServerMocksObj from './kbn_core_feature_flags_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index 845dc7df523a7..7efd3ed4eac80 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index 89710d3c51799..1900eca1e512d 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index 2c8ce91d1a9e3..62f92f93e281a 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 2008264f1fd5f..bdd589093c404 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index 37646c5adb934..3c72d4e264185 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index 53b0a7d481720..5abd44d15588a 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index e050f814b530a..346632ac28f25 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index cf072f090712b..d0e27729be146 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index bbf8c3602a573..77008db007bff 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.devdocs.json b/api_docs/kbn_core_http_router_server_internal.devdocs.json index 219635cec955e..37b9e5a564b6c 100644 --- a/api_docs/kbn_core_http_router_server_internal.devdocs.json +++ b/api_docs/kbn_core_http_router_server_internal.devdocs.json @@ -179,7 +179,7 @@ }, "<", "Method", - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -266,7 +266,7 @@ }, "<", "Method", - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -353,7 +353,7 @@ }, "<", "Method", - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -440,7 +440,7 @@ }, "<", "Method", - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -527,7 +527,7 @@ }, "<", "Method", - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 78a39b33d84fd..5c60a6ba6a416 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.devdocs.json b/api_docs/kbn_core_http_router_server_mocks.devdocs.json index df244e8c6f8c7..f32d021058562 100644 --- a/api_docs/kbn_core_http_router_server_mocks.devdocs.json +++ b/api_docs/kbn_core_http_router_server_mocks.devdocs.json @@ -72,7 +72,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 2d72bb71c1b30..8ca9a349bea6f 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.devdocs.json b/api_docs/kbn_core_http_server.devdocs.json index cf76b2a6b8cff..9516368c46188 100644 --- a/api_docs/kbn_core_http_server.devdocs.json +++ b/api_docs/kbn_core_http_server.devdocs.json @@ -5125,6 +5125,10 @@ "plugin": "@kbn/core-http-router-server-internal", "path": "packages/core/http/core-http-router-server-internal/src/router.test.ts" }, + { + "plugin": "@kbn/core-http-router-server-internal", + "path": "packages/core/http/core-http-router-server-internal/src/router.test.ts" + }, { "plugin": "@kbn/core-http-server-internal", "path": "packages/core/http/core-http-server-internal/src/http_server.test.ts" @@ -11569,12 +11573,12 @@ { "parentPluginId": "@kbn/core-http-server", "id": "def-server.KibanaRequestRoute.options", - "type": "Uncategorized", + "type": "CompoundType", "tags": [], "label": "options", "description": [], "signature": [ - "Method extends \"get\" | \"options\" ? Required>" + ">) & { security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; }" ], "path": "packages/core/http/core-http-server/src/router/request.ts", "deprecated": false, @@ -13415,29 +13427,6 @@ "deprecated": false, "trackAdoption": false }, - { - "parentPluginId": "@kbn/core-http-server", - "id": "def-server.RouteConfigOptions.security", - "type": "Object", - "tags": [], - "label": "security", - "description": [ - "\nDefines the security requirements for a route, including authorization and authentication.\n" - ], - "signature": [ - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.RouteSecurity", - "text": "RouteSecurity" - }, - " | undefined" - ], - "path": "packages/core/http/core-http-server/src/router/route.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "@kbn/core-http-server", "id": "def-server.RouteConfigOptions.httpResource", @@ -16323,7 +16312,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -16654,7 +16643,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -17821,7 +17810,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -17968,7 +17957,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -18267,7 +18256,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -18440,7 +18429,7 @@ "section": "def-server.RouteMethod", "text": "RouteMethod" }, - ">, \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; security?: ", + ">, \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; security?: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -19363,7 +19352,7 @@ "\nRoute options: If 'GET' or 'OPTIONS' method, body options won't be returned." ], "signature": [ - "Method extends \"get\" | \"options\" ? Required>" + ">) & { security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; }" ], "path": "packages/core/http/core-http-server/src/router/request.ts", "deprecated": false, @@ -20762,7 +20759,7 @@ "\nThe set of supported parseable Content-Types" ], "signature": [ - "\"application/json\" | \"multipart/form-data\" | \"application/*+json\" | \"application/octet-stream\" | \"application/x-www-form-urlencoded\" | \"text/*\"" + "\"application/json\" | \"application/*+json\" | \"application/octet-stream\" | \"application/x-www-form-urlencoded\" | \"multipart/form-data\" | \"text/*\"" ], "path": "packages/core/http/core-http-server/src/router/route.ts", "deprecated": false, @@ -21358,7 +21355,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -21442,7 +21439,7 @@ "section": "def-server.RouteConfigOptions", "text": "RouteConfigOptions" }, - ", \"security\" | \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", + ", \"description\" | \"summary\" | \"deprecated\" | \"access\" | \"discontinued\"> | undefined; access: ", { "pluginId": "@kbn/core-http-server", "scope": "server", diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 55a02fb53929a..8a562a43d39fb 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 568 | 2 | 242 | 0 | +| 567 | 2 | 242 | 0 | ## Server diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 5923b686ebff2..f6ce2b23f50d0 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index ee74529869530..ec4a5314bb22c 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_utils.devdocs.json b/api_docs/kbn_core_http_server_utils.devdocs.json new file mode 100644 index 0000000000000..176980e37b08a --- /dev/null +++ b/api_docs/kbn_core_http_server_utils.devdocs.json @@ -0,0 +1,186 @@ +{ + "id": "@kbn/core-http-server-utils", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.isCoreKibanaRequest", + "type": "Function", + "tags": [], + "label": "isCoreKibanaRequest", + "description": [], + "signature": [ + "(req: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => boolean" + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.isCoreKibanaRequest.$1", + "type": "Object", + "tags": [], + "label": "req", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.kibanaRequestFactory", + "type": "Function", + "tags": [], + "label": "kibanaRequestFactory", + "description": [ + "\nAllows building a KibanaRequest from a RawRequest, leveraging internal CoreKibanaRequest." + ], + "signature": [ + "(req: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RawRequest", + "text": "RawRequest" + }, + ", routeSchemas: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteValidator", + "text": "RouteValidator" + }, + " | undefined, withoutSecretHeaders: boolean) => ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.kibanaRequestFactory.$1", + "type": "CompoundType", + "tags": [], + "label": "req", + "description": [ + "The raw request to build from" + ], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RawRequest", + "text": "RawRequest" + } + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.kibanaRequestFactory.$2", + "type": "CompoundType", + "tags": [], + "label": "routeSchemas", + "description": [ + "The route schemas" + ], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteValidator", + "text": "RouteValidator" + }, + " | undefined" + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/core-http-server-utils", + "id": "def-server.kibanaRequestFactory.$3", + "type": "boolean", + "tags": [], + "label": "withoutSecretHeaders", + "description": [ + "Whether we want to exclude secret headers" + ], + "signature": [ + "boolean" + ], + "path": "packages/core/http/core-http-server-utils/src/request.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [ + "A KibanaRequest object" + ], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_http_server_utils.mdx b/api_docs/kbn_core_http_server_utils.mdx new file mode 100644 index 0000000000000..4977a0fc367a7 --- /dev/null +++ b/api_docs/kbn_core_http_server_utils.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreHttpServerUtilsPluginApi +slug: /kibana-dev-docs/api/kbn-core-http-server-utils +title: "@kbn/core-http-server-utils" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-http-server-utils plugin +date: 2024-11-26 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-utils'] +--- +import kbnCoreHttpServerUtilsObj from './kbn_core_http_server_utils.devdocs.json'; + + + +Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 2 | 0 | + +## Server + +### Functions + + diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index dbc324bbe189c..480691d703715 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index f89489a201acd..21782eb5714fa 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 9d28299f569b0..d4e3eaec74789 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index 7cde6c4bbe2a7..c9b9678768e34 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 567361809f5fb..39223f647df62 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index b0a5c82a6566f..8fd89cfd2324a 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 3753b5e3f2a30..bca0835f19cc7 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index 1310da82e234c..5969ee376fe37 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 9238d56eca725..5ddd5cb62d26e 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 3a740d0f20469..2ec7058015137 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index dd50f8a26b1fc..f116b7bc79905 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index 762ef3237137b..35c5cca2f2afc 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 06e3c60cc5550..2840db365ccb1 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index 489343ba64efc..3db0ca776e6ee 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 295626f7fe85e..221e308ce39fd 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 7d353d5470da3..5c1c51cbe9faf 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 968515173c251..94701be651f7c 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 0c861fac84ae1..2d03056387e61 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index a72eb5aaa90aa..6e65f11fece1b 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index a2800ad4b1eb0..0602832c594aa 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index c46d6130dd925..dc5b585178b32 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 7dece8d9fbc34..5f3b2f92cc841 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 788278bd5ebc2..cf4d949c30c18 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index a3e0ddba64e2b..eb566cc508caf 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index 4f8ded8e32bd2..6b22020a4e72c 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index b8fc3e4bff22b..bae877402fbbb 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index a2c46f685f5e7..480a622bf6c13 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index d90dcde8d4a99..bedba1ce4753a 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 591ebec8dad8b..97782f183823c 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 6c5ec3b332e57..67bd882d1ec17 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index ddfe408b0bc0d..ec6547dc54313 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index a981f3eadbfaf..4830fb4bffd31 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 676135236e924..723e9c2cfdc49 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index aed3d739273f4..3630ff553b686 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_contracts_browser.mdx b/api_docs/kbn_core_plugins_contracts_browser.mdx index 8a3a4b3e4bb53..d5eb5a79e9040 100644 --- a/api_docs/kbn_core_plugins_contracts_browser.mdx +++ b/api_docs/kbn_core_plugins_contracts_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-contracts-browser title: "@kbn/core-plugins-contracts-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-contracts-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-contracts-browser'] --- import kbnCorePluginsContractsBrowserObj from './kbn_core_plugins_contracts_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_contracts_server.mdx b/api_docs/kbn_core_plugins_contracts_server.mdx index 020659f3e6754..ff744ed15cd17 100644 --- a/api_docs/kbn_core_plugins_contracts_server.mdx +++ b/api_docs/kbn_core_plugins_contracts_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-contracts-server title: "@kbn/core-plugins-contracts-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-contracts-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-contracts-server'] --- import kbnCorePluginsContractsServerObj from './kbn_core_plugins_contracts_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index a39ab5801b0db..08e8c42ab3633 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 2c566d0093013..4c33b9e3f2e45 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 680b0f497153a..d5294c547f27c 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 64cd252ea3c6f..97f77d282995f 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser.mdx b/api_docs/kbn_core_rendering_browser.mdx index a6ecbed43a4a0..2aa7a6172c556 100644 --- a/api_docs/kbn_core_rendering_browser.mdx +++ b/api_docs/kbn_core_rendering_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser title: "@kbn/core-rendering-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser'] --- import kbnCoreRenderingBrowserObj from './kbn_core_rendering_browser.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index a4b8405f0a0b2..99f8bbdd68be5 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index a549d050f2b22..8852d3279832c 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index 42204498ed59d..30b6cb6e4cf5c 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 8be54d8773456..f105416f683db 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 5f6b42c52fd81..727938a418996 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index ee4927c9ac6a7..64ad14c3df8b3 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index e4d8d1c21f94a..b3df5ad99e3ee 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index f55accdd3da1e..3f16dc06f0e6a 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 99e8fff16012a..02abc62ebc686 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 318a991738322..9e0b4ffc5d0e7 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index d76859769d881..0e47974efcbe8 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 733f73af32a87..c4a33ed558616 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index f96ce7559d8fa..73c59b3c1fd76 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index bfece77773645..a8605fa69acd7 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 79142154da820..a2a79b40aa426 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 2873a2347baaf..13cf9755f2219 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 4b2d2d94fbb69..89314fb750287 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 92591e0511855..7f7ecd59f4fc9 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 810ae8217ea1c..71775403d09ac 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 30e04a0e456aa..cb59efb0ac457 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 4f8766de1d807..fbc2e8fcc7e52 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_security_browser.mdx b/api_docs/kbn_core_security_browser.mdx index 0d22011dabbae..43d191b769d43 100644 --- a/api_docs/kbn_core_security_browser.mdx +++ b/api_docs/kbn_core_security_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-browser title: "@kbn/core-security-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-browser'] --- import kbnCoreSecurityBrowserObj from './kbn_core_security_browser.devdocs.json'; diff --git a/api_docs/kbn_core_security_browser_internal.mdx b/api_docs/kbn_core_security_browser_internal.mdx index e5352aebc03c4..6930b5a6a570a 100644 --- a/api_docs/kbn_core_security_browser_internal.mdx +++ b/api_docs/kbn_core_security_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-browser-internal title: "@kbn/core-security-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-browser-internal'] --- import kbnCoreSecurityBrowserInternalObj from './kbn_core_security_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_security_browser_mocks.mdx b/api_docs/kbn_core_security_browser_mocks.mdx index f8e9a2bbb9e5b..f3b3f7c517ed1 100644 --- a/api_docs/kbn_core_security_browser_mocks.mdx +++ b/api_docs/kbn_core_security_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-browser-mocks title: "@kbn/core-security-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-browser-mocks'] --- import kbnCoreSecurityBrowserMocksObj from './kbn_core_security_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_security_common.mdx b/api_docs/kbn_core_security_common.mdx index c02feef9f4172..56d6259c388ce 100644 --- a/api_docs/kbn_core_security_common.mdx +++ b/api_docs/kbn_core_security_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-common title: "@kbn/core-security-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-common'] --- import kbnCoreSecurityCommonObj from './kbn_core_security_common.devdocs.json'; diff --git a/api_docs/kbn_core_security_server.mdx b/api_docs/kbn_core_security_server.mdx index 4271b39139b08..7bdd563cd14e3 100644 --- a/api_docs/kbn_core_security_server.mdx +++ b/api_docs/kbn_core_security_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-server title: "@kbn/core-security-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-server'] --- import kbnCoreSecurityServerObj from './kbn_core_security_server.devdocs.json'; diff --git a/api_docs/kbn_core_security_server_internal.mdx b/api_docs/kbn_core_security_server_internal.mdx index 399342f82e6ea..e24450339d309 100644 --- a/api_docs/kbn_core_security_server_internal.mdx +++ b/api_docs/kbn_core_security_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-server-internal title: "@kbn/core-security-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-server-internal'] --- import kbnCoreSecurityServerInternalObj from './kbn_core_security_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_security_server_mocks.mdx b/api_docs/kbn_core_security_server_mocks.mdx index 2f4ed482a6b6b..c487910daf3c6 100644 --- a/api_docs/kbn_core_security_server_mocks.mdx +++ b/api_docs/kbn_core_security_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-security-server-mocks title: "@kbn/core-security-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-security-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-security-server-mocks'] --- import kbnCoreSecurityServerMocksObj from './kbn_core_security_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 8ad680899b133..7b109b2f21f27 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 4f64deccda686..7d13b41e805e6 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index 57628ec13400b..8fca45621252b 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 7d3fe8ff0d57d..d658583a9e66a 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index d6ac39218d2a7..a2320c989db95 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index b2ff8153abec6..16ffcb2599742 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index a120e6dae99f8..03d960a583b84 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_model_versions.mdx b/api_docs/kbn_core_test_helpers_model_versions.mdx index 30c2ada98beb2..e829ad7a10064 100644 --- a/api_docs/kbn_core_test_helpers_model_versions.mdx +++ b/api_docs/kbn_core_test_helpers_model_versions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-model-versions title: "@kbn/core-test-helpers-model-versions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-model-versions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-model-versions'] --- import kbnCoreTestHelpersModelVersionsObj from './kbn_core_test_helpers_model_versions.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index c54812e7aa714..3c1b7e6c4576f 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 86af71bd50574..4d13057873d19 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index cbb2a3c6fc8e6..e183cf16ca3d0 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 8e3974864c788..068860c348d9b 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 064141c14e38d..8971528f75b7e 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index b251038a20467..da1cecb2601b9 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index acddb2e6c8898..029851960e19a 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index 017f50bd2f574..55f237c39ab67 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 3a54b1f77ae61..d6534396d5e47 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index a555684343093..9a35185a645f9 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index e4315a19ad0c9..1156d22947f04 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index d9722b63339e9..de6641a17e6f4 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index 47123344da9b7..2037d2b7ccbc0 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 4acc4b467c44b..e257357f19ef3 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_browser.mdx b/api_docs/kbn_core_user_profile_browser.mdx index d48ca72c29788..93e50303950f6 100644 --- a/api_docs/kbn_core_user_profile_browser.mdx +++ b/api_docs/kbn_core_user_profile_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-browser title: "@kbn/core-user-profile-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-browser'] --- import kbnCoreUserProfileBrowserObj from './kbn_core_user_profile_browser.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_browser_internal.mdx b/api_docs/kbn_core_user_profile_browser_internal.mdx index 3e27e905eeec9..ca316a9b7440d 100644 --- a/api_docs/kbn_core_user_profile_browser_internal.mdx +++ b/api_docs/kbn_core_user_profile_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-browser-internal title: "@kbn/core-user-profile-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-browser-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-browser-internal'] --- import kbnCoreUserProfileBrowserInternalObj from './kbn_core_user_profile_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_browser_mocks.mdx b/api_docs/kbn_core_user_profile_browser_mocks.mdx index fc2644c1c23d9..ce15c5d9187ca 100644 --- a/api_docs/kbn_core_user_profile_browser_mocks.mdx +++ b/api_docs/kbn_core_user_profile_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-browser-mocks title: "@kbn/core-user-profile-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-browser-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-browser-mocks'] --- import kbnCoreUserProfileBrowserMocksObj from './kbn_core_user_profile_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_common.mdx b/api_docs/kbn_core_user_profile_common.mdx index 03d21ce748b29..e176da7666e88 100644 --- a/api_docs/kbn_core_user_profile_common.mdx +++ b/api_docs/kbn_core_user_profile_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-common title: "@kbn/core-user-profile-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-common'] --- import kbnCoreUserProfileCommonObj from './kbn_core_user_profile_common.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_server.mdx b/api_docs/kbn_core_user_profile_server.mdx index d143883994524..e905ab234b2cf 100644 --- a/api_docs/kbn_core_user_profile_server.mdx +++ b/api_docs/kbn_core_user_profile_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-server title: "@kbn/core-user-profile-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-server'] --- import kbnCoreUserProfileServerObj from './kbn_core_user_profile_server.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_server_internal.mdx b/api_docs/kbn_core_user_profile_server_internal.mdx index d8b4ef2cf9083..47bdc7923c3d1 100644 --- a/api_docs/kbn_core_user_profile_server_internal.mdx +++ b/api_docs/kbn_core_user_profile_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-server-internal title: "@kbn/core-user-profile-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-server-internal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-server-internal'] --- import kbnCoreUserProfileServerInternalObj from './kbn_core_user_profile_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_user_profile_server_mocks.mdx b/api_docs/kbn_core_user_profile_server_mocks.mdx index 944bc788bc82b..fa9cdd8477c21 100644 --- a/api_docs/kbn_core_user_profile_server_mocks.mdx +++ b/api_docs/kbn_core_user_profile_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-profile-server-mocks title: "@kbn/core-user-profile-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-profile-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-profile-server-mocks'] --- import kbnCoreUserProfileServerMocksObj from './kbn_core_user_profile_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server.mdx b/api_docs/kbn_core_user_settings_server.mdx index 785269e9c6d00..72135202b986b 100644 --- a/api_docs/kbn_core_user_settings_server.mdx +++ b/api_docs/kbn_core_user_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server title: "@kbn/core-user-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server'] --- import kbnCoreUserSettingsServerObj from './kbn_core_user_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server_mocks.mdx b/api_docs/kbn_core_user_settings_server_mocks.mdx index ca638a3d00c88..9d9d877ece9f0 100644 --- a/api_docs/kbn_core_user_settings_server_mocks.mdx +++ b/api_docs/kbn_core_user_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server-mocks title: "@kbn/core-user-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server-mocks'] --- import kbnCoreUserSettingsServerMocksObj from './kbn_core_user_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 20628c7a5eeac..5d6b5950fb19e 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index f262bdbd51f10..fc844713c66e0 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_custom_icons.mdx b/api_docs/kbn_custom_icons.mdx index 3a0d466045b28..8547775b1ef88 100644 --- a/api_docs/kbn_custom_icons.mdx +++ b/api_docs/kbn_custom_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-custom-icons title: "@kbn/custom-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/custom-icons plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/custom-icons'] --- import kbnCustomIconsObj from './kbn_custom_icons.devdocs.json'; diff --git a/api_docs/kbn_custom_integrations.mdx b/api_docs/kbn_custom_integrations.mdx index ff0bd8f808072..5cae0b608c83f 100644 --- a/api_docs/kbn_custom_integrations.mdx +++ b/api_docs/kbn_custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-custom-integrations title: "@kbn/custom-integrations" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/custom-integrations plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/custom-integrations'] --- import kbnCustomIntegrationsObj from './kbn_custom_integrations.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 4a44abf43323d..a6024600ac34b 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_data_forge.mdx b/api_docs/kbn_data_forge.mdx index eba4a2cd58dcf..15245ae56c285 100644 --- a/api_docs/kbn_data_forge.mdx +++ b/api_docs/kbn_data_forge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-forge title: "@kbn/data-forge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-forge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-forge'] --- import kbnDataForgeObj from './kbn_data_forge.devdocs.json'; diff --git a/api_docs/kbn_data_service.mdx b/api_docs/kbn_data_service.mdx index 43a8ba3b45066..e06e5e9aa9943 100644 --- a/api_docs/kbn_data_service.mdx +++ b/api_docs/kbn_data_service.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-service title: "@kbn/data-service" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-service plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-service'] --- import kbnDataServiceObj from './kbn_data_service.devdocs.json'; diff --git a/api_docs/kbn_data_stream_adapter.mdx b/api_docs/kbn_data_stream_adapter.mdx index 29ccb7af11441..41c273ff1f69f 100644 --- a/api_docs/kbn_data_stream_adapter.mdx +++ b/api_docs/kbn_data_stream_adapter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-stream-adapter title: "@kbn/data-stream-adapter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-stream-adapter plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-stream-adapter'] --- import kbnDataStreamAdapterObj from './kbn_data_stream_adapter.devdocs.json'; diff --git a/api_docs/kbn_data_view_utils.mdx b/api_docs/kbn_data_view_utils.mdx index 739e5c9bb82db..3f27b427ed378 100644 --- a/api_docs/kbn_data_view_utils.mdx +++ b/api_docs/kbn_data_view_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-view-utils title: "@kbn/data-view-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-view-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-view-utils'] --- import kbnDataViewUtilsObj from './kbn_data_view_utils.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 0cc3704e3fe32..574e2dfa79286 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_analytics.mdx b/api_docs/kbn_deeplinks_analytics.mdx index 6b637b5f6c630..7440a8f11b69f 100644 --- a/api_docs/kbn_deeplinks_analytics.mdx +++ b/api_docs/kbn_deeplinks_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-analytics title: "@kbn/deeplinks-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-analytics plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-analytics'] --- import kbnDeeplinksAnalyticsObj from './kbn_deeplinks_analytics.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_devtools.mdx b/api_docs/kbn_deeplinks_devtools.mdx index 97257aca63efe..1b1f474bcea75 100644 --- a/api_docs/kbn_deeplinks_devtools.mdx +++ b/api_docs/kbn_deeplinks_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-devtools title: "@kbn/deeplinks-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-devtools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-devtools'] --- import kbnDeeplinksDevtoolsObj from './kbn_deeplinks_devtools.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_fleet.mdx b/api_docs/kbn_deeplinks_fleet.mdx index 69468a10bd86e..553e5d780394f 100644 --- a/api_docs/kbn_deeplinks_fleet.mdx +++ b/api_docs/kbn_deeplinks_fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-fleet title: "@kbn/deeplinks-fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-fleet plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-fleet'] --- import kbnDeeplinksFleetObj from './kbn_deeplinks_fleet.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_management.mdx b/api_docs/kbn_deeplinks_management.mdx index c6ef07bcbc119..c613fe0122020 100644 --- a/api_docs/kbn_deeplinks_management.mdx +++ b/api_docs/kbn_deeplinks_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-management title: "@kbn/deeplinks-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-management plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-management'] --- import kbnDeeplinksManagementObj from './kbn_deeplinks_management.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_ml.mdx b/api_docs/kbn_deeplinks_ml.mdx index 4ca9584912c02..760ef2a625f7c 100644 --- a/api_docs/kbn_deeplinks_ml.mdx +++ b/api_docs/kbn_deeplinks_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-ml title: "@kbn/deeplinks-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-ml plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-ml'] --- import kbnDeeplinksMlObj from './kbn_deeplinks_ml.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_observability.devdocs.json b/api_docs/kbn_deeplinks_observability.devdocs.json index 09daeec624069..820e8efc49cb6 100644 --- a/api_docs/kbn_deeplinks_observability.devdocs.json +++ b/api_docs/kbn_deeplinks_observability.devdocs.json @@ -857,7 +857,7 @@ "label": "AppId", "description": [], "signature": [ - "\"profiling\" | \"metrics\" | \"apm\" | \"synthetics\" | \"ux\" | \"logs\" | \"slo\" | \"observabilityAIAssistant\" | \"observability-overview\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\"" + "\"profiling\" | \"metrics\" | \"apm\" | \"synthetics\" | \"ux\" | \"logs\" | \"slo\" | \"observabilityAIAssistant\" | \"observability-overview\" | \"streams\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\"" ], "path": "packages/deeplinks/observability/deep_links.ts", "deprecated": false, @@ -938,7 +938,7 @@ "section": "def-common.AppId", "text": "AppId" }, - " | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\"" + " | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"streams:overview\"" ], "path": "packages/deeplinks/observability/deep_links.ts", "deprecated": false, diff --git a/api_docs/kbn_deeplinks_observability.mdx b/api_docs/kbn_deeplinks_observability.mdx index cfd4e59277615..ce46a707aed40 100644 --- a/api_docs/kbn_deeplinks_observability.mdx +++ b/api_docs/kbn_deeplinks_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-observability title: "@kbn/deeplinks-observability" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-observability plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-observability'] --- import kbnDeeplinksObservabilityObj from './kbn_deeplinks_observability.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_search.mdx b/api_docs/kbn_deeplinks_search.mdx index 0cbb4e082e5eb..1e6b5a0da7bbc 100644 --- a/api_docs/kbn_deeplinks_search.mdx +++ b/api_docs/kbn_deeplinks_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-search title: "@kbn/deeplinks-search" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-search plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-search'] --- import kbnDeeplinksSearchObj from './kbn_deeplinks_search.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_security.devdocs.json b/api_docs/kbn_deeplinks_security.devdocs.json index 8378fc426d00e..30343ed59fe16 100644 --- a/api_docs/kbn_deeplinks_security.devdocs.json +++ b/api_docs/kbn_deeplinks_security.devdocs.json @@ -58,7 +58,7 @@ "label": "DeepLinkId", "description": [], "signature": [ - "\"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\"" + "\"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:siem_migrations-rules\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\"" ], "path": "packages/deeplinks/security/index.ts", "deprecated": false, @@ -73,7 +73,7 @@ "label": "LinkId", "description": [], "signature": [ - "\"\" | \"cases\" | \"alerts\" | \"rules\" | \"policy\" | \"overview\" | \"dashboards\" | \"kubernetes\" | \"cases_create\" | \"cases_configure\" | \"hosts\" | \"users\" | \"cloud_defend-policies\" | \"cloud_security_posture-dashboard\" | \"cloud_security_posture-findings\" | \"cloud_security_posture-benchmarks\" | \"network\" | \"data_quality\" | \"explore\" | \"assets\" | \"cloud_defend\" | \"notes\" | \"administration\" | \"attack_discovery\" | \"blocklist\" | \"cloud_security_posture-rules\" | \"detections\" | \"detection_response\" | \"endpoints\" | \"event_filters\" | \"exceptions\" | \"host_isolation_exceptions\" | \"hosts-all\" | \"hosts-anomalies\" | \"hosts-risk\" | \"hosts-events\" | \"hosts-sessions\" | \"hosts-uncommon_processes\" | \"investigations\" | \"get_started\" | \"machine_learning-landing\" | \"network-anomalies\" | \"network-dns\" | \"network-events\" | \"network-flows\" | \"network-http\" | \"network-tls\" | \"response_actions_history\" | \"rules-add\" | \"rules-create\" | \"rules-landing\" | \"threat_intelligence\" | \"timelines\" | \"timelines-templates\" | \"trusted_apps\" | \"users-all\" | \"users-anomalies\" | \"users-authentications\" | \"users-events\" | \"users-risk\" | \"entity_analytics\" | \"entity_analytics-management\" | \"entity_analytics-asset-classification\" | \"entity_analytics-entity_store_management\" | \"coverage-overview\"" + "\"\" | \"cases\" | \"alerts\" | \"rules\" | \"policy\" | \"overview\" | \"dashboards\" | \"kubernetes\" | \"cases_create\" | \"cases_configure\" | \"hosts\" | \"users\" | \"cloud_defend-policies\" | \"cloud_security_posture-dashboard\" | \"cloud_security_posture-findings\" | \"cloud_security_posture-benchmarks\" | \"network\" | \"data_quality\" | \"explore\" | \"assets\" | \"cloud_defend\" | \"notes\" | \"administration\" | \"attack_discovery\" | \"blocklist\" | \"cloud_security_posture-rules\" | \"detections\" | \"detection_response\" | \"endpoints\" | \"event_filters\" | \"exceptions\" | \"host_isolation_exceptions\" | \"hosts-all\" | \"hosts-anomalies\" | \"hosts-risk\" | \"hosts-events\" | \"hosts-sessions\" | \"hosts-uncommon_processes\" | \"investigations\" | \"get_started\" | \"machine_learning-landing\" | \"network-anomalies\" | \"network-dns\" | \"network-events\" | \"network-flows\" | \"network-http\" | \"network-tls\" | \"response_actions_history\" | \"rules-add\" | \"rules-create\" | \"rules-landing\" | \"siem_migrations-rules\" | \"threat_intelligence\" | \"timelines\" | \"timelines-templates\" | \"trusted_apps\" | \"users-all\" | \"users-anomalies\" | \"users-authentications\" | \"users-events\" | \"users-risk\" | \"entity_analytics\" | \"entity_analytics-management\" | \"entity_analytics-asset-classification\" | \"entity_analytics-entity_store_management\" | \"coverage-overview\"" ], "path": "packages/deeplinks/security/index.ts", "deprecated": false, diff --git a/api_docs/kbn_deeplinks_security.mdx b/api_docs/kbn_deeplinks_security.mdx index 186f67fc41c36..cd9f9d765bdd9 100644 --- a/api_docs/kbn_deeplinks_security.mdx +++ b/api_docs/kbn_deeplinks_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-security title: "@kbn/deeplinks-security" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-security plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-security'] --- import kbnDeeplinksSecurityObj from './kbn_deeplinks_security.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_shared.mdx b/api_docs/kbn_deeplinks_shared.mdx index aaa60421f2a1a..775abc35805ce 100644 --- a/api_docs/kbn_deeplinks_shared.mdx +++ b/api_docs/kbn_deeplinks_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-shared title: "@kbn/deeplinks-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-shared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-shared'] --- import kbnDeeplinksSharedObj from './kbn_deeplinks_shared.devdocs.json'; diff --git a/api_docs/kbn_default_nav_analytics.mdx b/api_docs/kbn_default_nav_analytics.mdx index 6e3921a576d3e..9e2c4d9ec6cbc 100644 --- a/api_docs/kbn_default_nav_analytics.mdx +++ b/api_docs/kbn_default_nav_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-analytics title: "@kbn/default-nav-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-analytics plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-analytics'] --- import kbnDefaultNavAnalyticsObj from './kbn_default_nav_analytics.devdocs.json'; diff --git a/api_docs/kbn_default_nav_devtools.mdx b/api_docs/kbn_default_nav_devtools.mdx index 347d472e70cc8..9788992efbc8a 100644 --- a/api_docs/kbn_default_nav_devtools.mdx +++ b/api_docs/kbn_default_nav_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-devtools title: "@kbn/default-nav-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-devtools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-devtools'] --- import kbnDefaultNavDevtoolsObj from './kbn_default_nav_devtools.devdocs.json'; diff --git a/api_docs/kbn_default_nav_management.mdx b/api_docs/kbn_default_nav_management.mdx index d9873efe0b268..146e68be60282 100644 --- a/api_docs/kbn_default_nav_management.mdx +++ b/api_docs/kbn_default_nav_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-management title: "@kbn/default-nav-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-management plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-management'] --- import kbnDefaultNavManagementObj from './kbn_default_nav_management.devdocs.json'; diff --git a/api_docs/kbn_default_nav_ml.mdx b/api_docs/kbn_default_nav_ml.mdx index 70356eee17d34..635d11c0f003f 100644 --- a/api_docs/kbn_default_nav_ml.mdx +++ b/api_docs/kbn_default_nav_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-ml title: "@kbn/default-nav-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-ml plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-ml'] --- import kbnDefaultNavMlObj from './kbn_default_nav_ml.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 2f09ec4348e30..48c72e3b58e23 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index d3df9f01700cd..12f4173a5491b 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 551cf08b808b4..b61dd65e4bc3f 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 0df00d2d4382d..17e4c4bb4121b 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_discover_contextual_components.mdx b/api_docs/kbn_discover_contextual_components.mdx index 1d210a0def93f..adb3f8a97b7b3 100644 --- a/api_docs/kbn_discover_contextual_components.mdx +++ b/api_docs/kbn_discover_contextual_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-discover-contextual-components title: "@kbn/discover-contextual-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/discover-contextual-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/discover-contextual-components'] --- import kbnDiscoverContextualComponentsObj from './kbn_discover_contextual_components.devdocs.json'; diff --git a/api_docs/kbn_discover_utils.mdx b/api_docs/kbn_discover_utils.mdx index 4d58bcac8bb80..95caad56a3012 100644 --- a/api_docs/kbn_discover_utils.mdx +++ b/api_docs/kbn_discover_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-discover-utils title: "@kbn/discover-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/discover-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/discover-utils'] --- import kbnDiscoverUtilsObj from './kbn_discover_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index 50ca91a1d6821..2c3281ac89a96 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -1024,6 +1024,20 @@ "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/doc-links", + "id": "def-common.DocLinks.inferenceManagement", + "type": "Object", + "tags": [], + "label": "inferenceManagement", + "description": [], + "signature": [ + "{ readonly inferenceAPIDocumentation: string; }" + ], + "path": "packages/kbn-doc-links/src/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 578be64ac2265..5871dc272d70d 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/docs](https://github.com/orgs/elastic/teams/docs) for question | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 78 | 0 | 78 | 2 | +| 79 | 0 | 79 | 2 | ## Common diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 12e75295de4c7..fb871eea8a504 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index 524192690ae95..81ffdf51aed03 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index f9b929d16e76b..eefd3b81ad839 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index dc55efcd87a55..dffd2f935e9f1 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_elastic_agent_utils.mdx b/api_docs/kbn_elastic_agent_utils.mdx index d174998b5ae7f..a04d031096f72 100644 --- a/api_docs/kbn_elastic_agent_utils.mdx +++ b/api_docs/kbn_elastic_agent_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-agent-utils title: "@kbn/elastic-agent-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-agent-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-agent-utils'] --- import kbnElasticAgentUtilsObj from './kbn_elastic_agent_utils.devdocs.json'; diff --git a/api_docs/kbn_elastic_assistant.mdx b/api_docs/kbn_elastic_assistant.mdx index 958429e48df24..cbec230aa1ddd 100644 --- a/api_docs/kbn_elastic_assistant.mdx +++ b/api_docs/kbn_elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-assistant title: "@kbn/elastic-assistant" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-assistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant'] --- import kbnElasticAssistantObj from './kbn_elastic_assistant.devdocs.json'; diff --git a/api_docs/kbn_elastic_assistant_common.mdx b/api_docs/kbn_elastic_assistant_common.mdx index 714f47255cb53..1e246228760bc 100644 --- a/api_docs/kbn_elastic_assistant_common.mdx +++ b/api_docs/kbn_elastic_assistant_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-assistant-common title: "@kbn/elastic-assistant-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-assistant-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant-common'] --- import kbnElasticAssistantCommonObj from './kbn_elastic_assistant_common.devdocs.json'; diff --git a/api_docs/kbn_entities_schema.devdocs.json b/api_docs/kbn_entities_schema.devdocs.json index ae8daadc3978e..5bc7ba4f31a9f 100644 --- a/api_docs/kbn_entities_schema.devdocs.json +++ b/api_docs/kbn_entities_schema.devdocs.json @@ -120,6 +120,67 @@ } ], "interfaces": [ + { + "parentPluginId": "@kbn/entities-schema", + "id": "def-common.EntityV2", + "type": "Interface", + "tags": [], + "label": "EntityV2", + "description": [], + "path": "x-pack/packages/kbn-entities-schema/src/schema/entity.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/entities-schema", + "id": "def-common.EntityV2.entity.id", + "type": "string", + "tags": [], + "label": "'entity.id'", + "description": [], + "path": "x-pack/packages/kbn-entities-schema/src/schema/entity.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/entities-schema", + "id": "def-common.EntityV2.entity.last_seen_timestamp", + "type": "string", + "tags": [], + "label": "'entity.last_seen_timestamp'", + "description": [], + "path": "x-pack/packages/kbn-entities-schema/src/schema/entity.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/entities-schema", + "id": "def-common.EntityV2.entity.type", + "type": "string", + "tags": [], + "label": "'entity.type'", + "description": [], + "path": "x-pack/packages/kbn-entities-schema/src/schema/entity.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/entities-schema", + "id": "def-common.EntityV2.Unnamed", + "type": "IndexSignature", + "tags": [], + "label": "[metadata: string]: any", + "description": [], + "signature": [ + "[metadata: string]: any" + ], + "path": "x-pack/packages/kbn-entities-schema/src/schema/entity.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/entities-schema", "id": "def-common.MetadataRecord", diff --git a/api_docs/kbn_entities_schema.mdx b/api_docs/kbn_entities_schema.mdx index faee25a2ad6a3..fe47eda50bc3b 100644 --- a/api_docs/kbn_entities_schema.mdx +++ b/api_docs/kbn_entities_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-entities-schema title: "@kbn/entities-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/entities-schema plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/entities-schema'] --- import kbnEntitiesSchemaObj from './kbn_entities_schema.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entiti | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 45 | 0 | 45 | 0 | +| 50 | 0 | 50 | 0 | ## Common diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 63753f340e139..08801d68d9225 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 0cb542fa8fe55..4032015f2f386 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 819e0cde2caf4..1e212f9c25fce 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index e009afd71d46f..a18ed7ef8bf97 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.devdocs.json b/api_docs/kbn_es_types.devdocs.json index 60ef30c14df1c..ec62dd1cd61ff 100644 --- a/api_docs/kbn_es_types.devdocs.json +++ b/api_docs/kbn_es_types.devdocs.json @@ -137,12 +137,13 @@ { "parentPluginId": "@kbn/es-types", "id": "def-common.ESQLSearchParams.params", - "type": "Array", + "type": "CompoundType", "tags": [], "label": "params", "description": [], "signature": [ - "Record[] | undefined" + "ScalarValue", + "[] | Record[] | undefined" ], "path": "packages/kbn-es-types/src/search.ts", "deprecated": false, diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index a8438442ffc2d..b11852542ff20 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index fe1f4988f9787..b42c3cb91b8fb 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_esql_ast.mdx b/api_docs/kbn_esql_ast.mdx index 56f6ff9c8c9b3..20f834e8f624c 100644 --- a/api_docs/kbn_esql_ast.mdx +++ b/api_docs/kbn_esql_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-esql-ast title: "@kbn/esql-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/esql-ast plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/esql-ast'] --- import kbnEsqlAstObj from './kbn_esql_ast.devdocs.json'; diff --git a/api_docs/kbn_esql_editor.mdx b/api_docs/kbn_esql_editor.mdx index 7a06bb880eaaa..d498a1578740a 100644 --- a/api_docs/kbn_esql_editor.mdx +++ b/api_docs/kbn_esql_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-esql-editor title: "@kbn/esql-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/esql-editor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/esql-editor'] --- import kbnEsqlEditorObj from './kbn_esql_editor.devdocs.json'; diff --git a/api_docs/kbn_esql_utils.mdx b/api_docs/kbn_esql_utils.mdx index 497bc1fece29b..0480711869f37 100644 --- a/api_docs/kbn_esql_utils.mdx +++ b/api_docs/kbn_esql_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-esql-utils title: "@kbn/esql-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/esql-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/esql-utils'] --- import kbnEsqlUtilsObj from './kbn_esql_utils.devdocs.json'; diff --git a/api_docs/kbn_esql_validation_autocomplete.mdx b/api_docs/kbn_esql_validation_autocomplete.mdx index 61e10af73c31c..d4121d5e47864 100644 --- a/api_docs/kbn_esql_validation_autocomplete.mdx +++ b/api_docs/kbn_esql_validation_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-esql-validation-autocomplete title: "@kbn/esql-validation-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/esql-validation-autocomplete plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/esql-validation-autocomplete'] --- import kbnEsqlValidationAutocompleteObj from './kbn_esql_validation_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_common.mdx b/api_docs/kbn_event_annotation_common.mdx index 981fbeb59012f..5268876010daa 100644 --- a/api_docs/kbn_event_annotation_common.mdx +++ b/api_docs/kbn_event_annotation_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-common title: "@kbn/event-annotation-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-common'] --- import kbnEventAnnotationCommonObj from './kbn_event_annotation_common.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_components.mdx b/api_docs/kbn_event_annotation_components.mdx index 485def755f1f8..228d4b621ab0e 100644 --- a/api_docs/kbn_event_annotation_components.mdx +++ b/api_docs/kbn_event_annotation_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-components title: "@kbn/event-annotation-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-components'] --- import kbnEventAnnotationComponentsObj from './kbn_event_annotation_components.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index 219871bfd9ba0..62abeb43f614e 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 5336e32353432..b9a651d13ca9b 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_field_utils.mdx b/api_docs/kbn_field_utils.mdx index 530f30ed9f5fb..69a0a5df9776a 100644 --- a/api_docs/kbn_field_utils.mdx +++ b/api_docs/kbn_field_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-utils title: "@kbn/field-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-utils'] --- import kbnFieldUtilsObj from './kbn_field_utils.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 593fbac108a82..87c6de8073c60 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_formatters.mdx b/api_docs/kbn_formatters.mdx index b7ff845156903..befe2998feec4 100644 --- a/api_docs/kbn_formatters.mdx +++ b/api_docs/kbn_formatters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-formatters title: "@kbn/formatters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/formatters plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/formatters'] --- import kbnFormattersObj from './kbn_formatters.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 319123ca29b67..ba202f8df7f6d 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_ui_services.mdx b/api_docs/kbn_ftr_common_functional_ui_services.mdx index 26b6daa03c537..829005b1172f1 100644 --- a/api_docs/kbn_ftr_common_functional_ui_services.mdx +++ b/api_docs/kbn_ftr_common_functional_ui_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-ui-services title: "@kbn/ftr-common-functional-ui-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-ui-services plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-ui-services'] --- import kbnFtrCommonFunctionalUiServicesObj from './kbn_ftr_common_functional_ui_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index d5a36e95d2fc3..42890de4a8a45 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_console_definitions.mdx b/api_docs/kbn_generate_console_definitions.mdx index 5a31d92444519..0358d5918a030 100644 --- a/api_docs/kbn_generate_console_definitions.mdx +++ b/api_docs/kbn_generate_console_definitions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-console-definitions title: "@kbn/generate-console-definitions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-console-definitions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-console-definitions'] --- import kbnGenerateConsoleDefinitionsObj from './kbn_generate_console_definitions.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.devdocs.json b/api_docs/kbn_generate_csv.devdocs.json index a95a53670c1b6..535dee81a98dc 100644 --- a/api_docs/kbn_generate_csv.devdocs.json +++ b/api_docs/kbn_generate_csv.devdocs.json @@ -64,7 +64,7 @@ "label": "config", "description": [], "signature": [ - "Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -240,7 +240,7 @@ "label": "config", "description": [], "signature": [ - "Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index bde383f9f1b62..8f050dcf9035d 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_grid_layout.mdx b/api_docs/kbn_grid_layout.mdx index 89cee389aa55b..3e594a0f60ca4 100644 --- a/api_docs/kbn_grid_layout.mdx +++ b/api_docs/kbn_grid_layout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-grid-layout title: "@kbn/grid-layout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/grid-layout plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/grid-layout'] --- import kbnGridLayoutObj from './kbn_grid_layout.devdocs.json'; diff --git a/api_docs/kbn_grouping.mdx b/api_docs/kbn_grouping.mdx index 203be187cdc77..881e6c2117037 100644 --- a/api_docs/kbn_grouping.mdx +++ b/api_docs/kbn_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-grouping title: "@kbn/grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/grouping plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/grouping'] --- import kbnGroupingObj from './kbn_grouping.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 0e26b3e0f20c2..7f6e1dd104f33 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 5dd459ddcaa1e..7f8f716549a48 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 0d19292e6abde..a49ee1c98010e 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index d72e55d814dfa..584edc08a3bf9 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index ca0dd9585e71a..0059f412e6980 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 4b4f9e2d6f07d..1216eaceccf0c 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 079a606760cfa..fb7cb2cbfe0c4 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 8a34d541608ee..8e887e24edc6a 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 50d97c0830c62..bb916f942f6c0 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_index_adapter.mdx b/api_docs/kbn_index_adapter.mdx index b252220048a9a..4a02a43542bc4 100644 --- a/api_docs/kbn_index_adapter.mdx +++ b/api_docs/kbn_index_adapter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-index-adapter title: "@kbn/index-adapter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/index-adapter plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/index-adapter'] --- import kbnIndexAdapterObj from './kbn_index_adapter.devdocs.json'; diff --git a/api_docs/kbn_index_lifecycle_management_common_shared.mdx b/api_docs/kbn_index_lifecycle_management_common_shared.mdx index 94cca4ab54ef2..9f4c1059e25e4 100644 --- a/api_docs/kbn_index_lifecycle_management_common_shared.mdx +++ b/api_docs/kbn_index_lifecycle_management_common_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-index-lifecycle-management-common-shared title: "@kbn/index-lifecycle-management-common-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/index-lifecycle-management-common-shared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/index-lifecycle-management-common-shared'] --- import kbnIndexLifecycleManagementCommonSharedObj from './kbn_index_lifecycle_management_common_shared.devdocs.json'; diff --git a/api_docs/kbn_index_management_shared_types.mdx b/api_docs/kbn_index_management_shared_types.mdx index 9203de44ee156..4075f36aa7eb9 100644 --- a/api_docs/kbn_index_management_shared_types.mdx +++ b/api_docs/kbn_index_management_shared_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-index-management-shared-types title: "@kbn/index-management-shared-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/index-management-shared-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/index-management-shared-types'] --- import kbnIndexManagementSharedTypesObj from './kbn_index_management_shared_types.devdocs.json'; diff --git a/api_docs/kbn_inference_common.mdx b/api_docs/kbn_inference_common.mdx index 0009101e3db70..fbafb23930796 100644 --- a/api_docs/kbn_inference_common.mdx +++ b/api_docs/kbn_inference_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-inference-common title: "@kbn/inference-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/inference-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/inference-common'] --- import kbnInferenceCommonObj from './kbn_inference_common.devdocs.json'; diff --git a/api_docs/kbn_inference_integration_flyout.mdx b/api_docs/kbn_inference_integration_flyout.mdx index 120d84215ca4e..381d1f5f1e09d 100644 --- a/api_docs/kbn_inference_integration_flyout.mdx +++ b/api_docs/kbn_inference_integration_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-inference_integration_flyout title: "@kbn/inference_integration_flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/inference_integration_flyout plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/inference_integration_flyout'] --- import kbnInferenceIntegrationFlyoutObj from './kbn_inference_integration_flyout.devdocs.json'; diff --git a/api_docs/kbn_infra_forge.mdx b/api_docs/kbn_infra_forge.mdx index 6dcaf322f9898..30f9899242b77 100644 --- a/api_docs/kbn_infra_forge.mdx +++ b/api_docs/kbn_infra_forge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-infra-forge title: "@kbn/infra-forge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/infra-forge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/infra-forge'] --- import kbnInfraForgeObj from './kbn_infra_forge.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 90913295ff20e..9fc6b19407f71 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_investigation_shared.mdx b/api_docs/kbn_investigation_shared.mdx index 93a542e6beffd..05476bbb7194e 100644 --- a/api_docs/kbn_investigation_shared.mdx +++ b/api_docs/kbn_investigation_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-investigation-shared title: "@kbn/investigation-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/investigation-shared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/investigation-shared'] --- import kbnInvestigationSharedObj from './kbn_investigation_shared.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 66647e17f605f..685b884c1a3d2 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_ipynb.mdx b/api_docs/kbn_ipynb.mdx index 917ee150d86af..a173fce5d5ca2 100644 --- a/api_docs/kbn_ipynb.mdx +++ b/api_docs/kbn_ipynb.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ipynb title: "@kbn/ipynb" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ipynb plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ipynb'] --- import kbnIpynbObj from './kbn_ipynb.devdocs.json'; diff --git a/api_docs/kbn_item_buffer.mdx b/api_docs/kbn_item_buffer.mdx index e7273e3ee1963..bdbe57dd15a7a 100644 --- a/api_docs/kbn_item_buffer.mdx +++ b/api_docs/kbn_item_buffer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-item-buffer title: "@kbn/item-buffer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/item-buffer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/item-buffer'] --- import kbnItemBufferObj from './kbn_item_buffer.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index aa0e2db75dcb8..a6d26dbe5d774 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 69534c60761bb..6b22b642635c5 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index 061f972ff92fe..11d6493238c4d 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_json_schemas.mdx b/api_docs/kbn_json_schemas.mdx index 13e9452daedca..5500be114a847 100644 --- a/api_docs/kbn_json_schemas.mdx +++ b/api_docs/kbn_json_schemas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-schemas title: "@kbn/json-schemas" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-schemas plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-schemas'] --- import kbnJsonSchemasObj from './kbn_json_schemas.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 9ce785871fda2..57bf04da6d251 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation.mdx b/api_docs/kbn_language_documentation.mdx index 2f811942bd934..090c6d2702648 100644 --- a/api_docs/kbn_language_documentation.mdx +++ b/api_docs/kbn_language_documentation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation title: "@kbn/language-documentation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation'] --- import kbnLanguageDocumentationObj from './kbn_language_documentation.devdocs.json'; diff --git a/api_docs/kbn_lens_embeddable_utils.mdx b/api_docs/kbn_lens_embeddable_utils.mdx index f7b95eaf89eed..59c1110e68f2d 100644 --- a/api_docs/kbn_lens_embeddable_utils.mdx +++ b/api_docs/kbn_lens_embeddable_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-lens-embeddable-utils title: "@kbn/lens-embeddable-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/lens-embeddable-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/lens-embeddable-utils'] --- import kbnLensEmbeddableUtilsObj from './kbn_lens_embeddable_utils.devdocs.json'; diff --git a/api_docs/kbn_lens_formula_docs.mdx b/api_docs/kbn_lens_formula_docs.mdx index 2bf3769f80140..8b74ddb43264b 100644 --- a/api_docs/kbn_lens_formula_docs.mdx +++ b/api_docs/kbn_lens_formula_docs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-lens-formula-docs title: "@kbn/lens-formula-docs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/lens-formula-docs plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/lens-formula-docs'] --- import kbnLensFormulaDocsObj from './kbn_lens_formula_docs.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 309660590d5cb..d02514d9dd632 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index f109ef3c783b8..6a37fb4fb0266 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_content_badge.mdx b/api_docs/kbn_managed_content_badge.mdx index 9e093cfc91a27..2e40f2092ce13 100644 --- a/api_docs/kbn_managed_content_badge.mdx +++ b/api_docs/kbn_managed_content_badge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-content-badge title: "@kbn/managed-content-badge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-content-badge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-content-badge'] --- import kbnManagedContentBadgeObj from './kbn_managed_content_badge.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 96286947758ac..ca1af907c2054 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_management_cards_navigation.mdx b/api_docs/kbn_management_cards_navigation.mdx index 2f0644094b26e..ec36db9e3b5db 100644 --- a/api_docs/kbn_management_cards_navigation.mdx +++ b/api_docs/kbn_management_cards_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-cards-navigation title: "@kbn/management-cards-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-cards-navigation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-cards-navigation'] --- import kbnManagementCardsNavigationObj from './kbn_management_cards_navigation.devdocs.json'; diff --git a/api_docs/kbn_management_settings_application.mdx b/api_docs/kbn_management_settings_application.mdx index e73b051b1f28d..5193e5ff8f9e2 100644 --- a/api_docs/kbn_management_settings_application.mdx +++ b/api_docs/kbn_management_settings_application.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-application title: "@kbn/management-settings-application" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-application plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-application'] --- import kbnManagementSettingsApplicationObj from './kbn_management_settings_application.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_category.mdx b/api_docs/kbn_management_settings_components_field_category.mdx index 15dee22ab50a3..84444c534151e 100644 --- a/api_docs/kbn_management_settings_components_field_category.mdx +++ b/api_docs/kbn_management_settings_components_field_category.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-category title: "@kbn/management-settings-components-field-category" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-category plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-category'] --- import kbnManagementSettingsComponentsFieldCategoryObj from './kbn_management_settings_components_field_category.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_input.mdx b/api_docs/kbn_management_settings_components_field_input.mdx index 7c3d514f794d5..775375de40128 100644 --- a/api_docs/kbn_management_settings_components_field_input.mdx +++ b/api_docs/kbn_management_settings_components_field_input.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-input title: "@kbn/management-settings-components-field-input" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-input plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-input'] --- import kbnManagementSettingsComponentsFieldInputObj from './kbn_management_settings_components_field_input.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_row.mdx b/api_docs/kbn_management_settings_components_field_row.mdx index a76fa0e04720d..a9d1e0deda03b 100644 --- a/api_docs/kbn_management_settings_components_field_row.mdx +++ b/api_docs/kbn_management_settings_components_field_row.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-row title: "@kbn/management-settings-components-field-row" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-row plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-row'] --- import kbnManagementSettingsComponentsFieldRowObj from './kbn_management_settings_components_field_row.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_form.mdx b/api_docs/kbn_management_settings_components_form.mdx index aa88fda990f96..3994861aad1ef 100644 --- a/api_docs/kbn_management_settings_components_form.mdx +++ b/api_docs/kbn_management_settings_components_form.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-form title: "@kbn/management-settings-components-form" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-form plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-form'] --- import kbnManagementSettingsComponentsFormObj from './kbn_management_settings_components_form.devdocs.json'; diff --git a/api_docs/kbn_management_settings_field_definition.mdx b/api_docs/kbn_management_settings_field_definition.mdx index 2da954b6c544e..aeb6bebfbe4c2 100644 --- a/api_docs/kbn_management_settings_field_definition.mdx +++ b/api_docs/kbn_management_settings_field_definition.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-field-definition title: "@kbn/management-settings-field-definition" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-field-definition plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-field-definition'] --- import kbnManagementSettingsFieldDefinitionObj from './kbn_management_settings_field_definition.devdocs.json'; diff --git a/api_docs/kbn_management_settings_ids.mdx b/api_docs/kbn_management_settings_ids.mdx index 4a96d23c42cac..820a887822f7a 100644 --- a/api_docs/kbn_management_settings_ids.mdx +++ b/api_docs/kbn_management_settings_ids.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-management-settings-ids title: "@kbn/management-settings-ids" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-ids plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-ids'] --- import kbnManagementSettingsIdsObj from './kbn_management_settings_ids.devdocs.json'; -Contact [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) for questions regarding this plugin. +Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_management_settings_section_registry.mdx b/api_docs/kbn_management_settings_section_registry.mdx index d1d7a20a5813d..ab4b2d9db3eb1 100644 --- a/api_docs/kbn_management_settings_section_registry.mdx +++ b/api_docs/kbn_management_settings_section_registry.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-management-settings-section-registry title: "@kbn/management-settings-section-registry" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-section-registry plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-section-registry'] --- import kbnManagementSettingsSectionRegistryObj from './kbn_management_settings_section_registry.devdocs.json'; -Contact [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) for questions regarding this plugin. +Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_management_settings_types.mdx b/api_docs/kbn_management_settings_types.mdx index ff19cb8ad3922..50002e44d3671 100644 --- a/api_docs/kbn_management_settings_types.mdx +++ b/api_docs/kbn_management_settings_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-types title: "@kbn/management-settings-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-types'] --- import kbnManagementSettingsTypesObj from './kbn_management_settings_types.devdocs.json'; diff --git a/api_docs/kbn_management_settings_utilities.mdx b/api_docs/kbn_management_settings_utilities.mdx index 06cad3ba4f045..816b88e233134 100644 --- a/api_docs/kbn_management_settings_utilities.mdx +++ b/api_docs/kbn_management_settings_utilities.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-utilities title: "@kbn/management-settings-utilities" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-utilities plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-utilities'] --- import kbnManagementSettingsUtilitiesObj from './kbn_management_settings_utilities.devdocs.json'; diff --git a/api_docs/kbn_management_storybook_config.mdx b/api_docs/kbn_management_storybook_config.mdx index 734d38e182b34..d920b6d2d314c 100644 --- a/api_docs/kbn_management_storybook_config.mdx +++ b/api_docs/kbn_management_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-storybook-config title: "@kbn/management-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-storybook-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-storybook-config'] --- import kbnManagementStorybookConfigObj from './kbn_management_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_manifest.mdx b/api_docs/kbn_manifest.mdx index f21e3a9c18255..aff9287c745c8 100644 --- a/api_docs/kbn_manifest.mdx +++ b/api_docs/kbn_manifest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-manifest title: "@kbn/manifest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/manifest plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/manifest'] --- import kbnManifestObj from './kbn_manifest.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 04d70b1064a64..9abcee14c8e41 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_maps_vector_tile_utils.mdx b/api_docs/kbn_maps_vector_tile_utils.mdx index aed1ee5096639..f63532d26a526 100644 --- a/api_docs/kbn_maps_vector_tile_utils.mdx +++ b/api_docs/kbn_maps_vector_tile_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-maps-vector-tile-utils title: "@kbn/maps-vector-tile-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/maps-vector-tile-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/maps-vector-tile-utils'] --- import kbnMapsVectorTileUtilsObj from './kbn_maps_vector_tile_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 06f5c42e15a22..07170bae0914c 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_anomaly_utils.mdx b/api_docs/kbn_ml_anomaly_utils.mdx index e33da401d2b22..103b41617b971 100644 --- a/api_docs/kbn_ml_anomaly_utils.mdx +++ b/api_docs/kbn_ml_anomaly_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-anomaly-utils title: "@kbn/ml-anomaly-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-anomaly-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-anomaly-utils'] --- import kbnMlAnomalyUtilsObj from './kbn_ml_anomaly_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_cancellable_search.mdx b/api_docs/kbn_ml_cancellable_search.mdx index 7316e0f631157..ca3823fca9cb7 100644 --- a/api_docs/kbn_ml_cancellable_search.mdx +++ b/api_docs/kbn_ml_cancellable_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-cancellable-search title: "@kbn/ml-cancellable-search" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-cancellable-search plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-cancellable-search'] --- import kbnMlCancellableSearchObj from './kbn_ml_cancellable_search.devdocs.json'; diff --git a/api_docs/kbn_ml_category_validator.mdx b/api_docs/kbn_ml_category_validator.mdx index cb4f89e67e3ba..b00f17a2b7b5e 100644 --- a/api_docs/kbn_ml_category_validator.mdx +++ b/api_docs/kbn_ml_category_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-category-validator title: "@kbn/ml-category-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-category-validator plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-category-validator'] --- import kbnMlCategoryValidatorObj from './kbn_ml_category_validator.devdocs.json'; diff --git a/api_docs/kbn_ml_chi2test.mdx b/api_docs/kbn_ml_chi2test.mdx index ce63110538ba0..627406f8d0146 100644 --- a/api_docs/kbn_ml_chi2test.mdx +++ b/api_docs/kbn_ml_chi2test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-chi2test title: "@kbn/ml-chi2test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-chi2test plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-chi2test'] --- import kbnMlChi2testObj from './kbn_ml_chi2test.devdocs.json'; diff --git a/api_docs/kbn_ml_data_frame_analytics_utils.mdx b/api_docs/kbn_ml_data_frame_analytics_utils.mdx index 107fa77d19dce..0241cecdf5ddc 100644 --- a/api_docs/kbn_ml_data_frame_analytics_utils.mdx +++ b/api_docs/kbn_ml_data_frame_analytics_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-frame-analytics-utils title: "@kbn/ml-data-frame-analytics-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-frame-analytics-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-frame-analytics-utils'] --- import kbnMlDataFrameAnalyticsUtilsObj from './kbn_ml_data_frame_analytics_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_data_grid.mdx b/api_docs/kbn_ml_data_grid.mdx index 1c6d824deb927..4b147ace76c9c 100644 --- a/api_docs/kbn_ml_data_grid.mdx +++ b/api_docs/kbn_ml_data_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-grid title: "@kbn/ml-data-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-grid plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-grid'] --- import kbnMlDataGridObj from './kbn_ml_data_grid.devdocs.json'; diff --git a/api_docs/kbn_ml_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index 21a8a50f5fe46..6f8012fa8bbcf 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_date_utils.mdx b/api_docs/kbn_ml_date_utils.mdx index 23c2f2bffa090..00a4bbf3adf11 100644 --- a/api_docs/kbn_ml_date_utils.mdx +++ b/api_docs/kbn_ml_date_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-utils title: "@kbn/ml-date-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-utils'] --- import kbnMlDateUtilsObj from './kbn_ml_date_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx index 1ca2e4d1d5566..30f0bf06b92a6 100644 --- a/api_docs/kbn_ml_error_utils.mdx +++ b/api_docs/kbn_ml_error_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-error-utils title: "@kbn/ml-error-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-error-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] --- import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_field_stats_flyout.mdx b/api_docs/kbn_ml_field_stats_flyout.mdx index e71bcc3dd5971..66fb04140bd35 100644 --- a/api_docs/kbn_ml_field_stats_flyout.mdx +++ b/api_docs/kbn_ml_field_stats_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-field-stats-flyout title: "@kbn/ml-field-stats-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-field-stats-flyout plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-field-stats-flyout'] --- import kbnMlFieldStatsFlyoutObj from './kbn_ml_field_stats_flyout.devdocs.json'; diff --git a/api_docs/kbn_ml_in_memory_table.mdx b/api_docs/kbn_ml_in_memory_table.mdx index 8b54b3c3bd30a..193779728388d 100644 --- a/api_docs/kbn_ml_in_memory_table.mdx +++ b/api_docs/kbn_ml_in_memory_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-in-memory-table title: "@kbn/ml-in-memory-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-in-memory-table plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-in-memory-table'] --- import kbnMlInMemoryTableObj from './kbn_ml_in_memory_table.devdocs.json'; diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index a3ca6622d64b1..35993d787a090 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 6be5e86f221a5..5bebef5106bdc 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_kibana_theme.mdx b/api_docs/kbn_ml_kibana_theme.mdx index d47e358375ee5..5b24a4e177e0f 100644 --- a/api_docs/kbn_ml_kibana_theme.mdx +++ b/api_docs/kbn_ml_kibana_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-kibana-theme title: "@kbn/ml-kibana-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-kibana-theme plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-kibana-theme'] --- import kbnMlKibanaThemeObj from './kbn_ml_kibana_theme.devdocs.json'; diff --git a/api_docs/kbn_ml_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index 4df32753933d9..c8e3b38ead011 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index 00ab3e7e96d04..5969cc11d46b8 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index f44c4c4f7181b..ea1e230ef9fa5 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_parse_interval.mdx b/api_docs/kbn_ml_parse_interval.mdx index 17cb1ddb12d14..f132ae5b8d04e 100644 --- a/api_docs/kbn_ml_parse_interval.mdx +++ b/api_docs/kbn_ml_parse_interval.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-parse-interval title: "@kbn/ml-parse-interval" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-parse-interval plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-parse-interval'] --- import kbnMlParseIntervalObj from './kbn_ml_parse_interval.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index 7c59b5dcc8315..f17bc1f24795a 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index 339163fc8f9b9..6edeadd03cb32 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index b03b8628b44cc..a2b8962092367 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_runtime_field_utils.mdx b/api_docs/kbn_ml_runtime_field_utils.mdx index 2ef7d97f23677..a5f8e062d3cef 100644 --- a/api_docs/kbn_ml_runtime_field_utils.mdx +++ b/api_docs/kbn_ml_runtime_field_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-runtime-field-utils title: "@kbn/ml-runtime-field-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-runtime-field-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-runtime-field-utils'] --- import kbnMlRuntimeFieldUtilsObj from './kbn_ml_runtime_field_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 2bc0d1e76e66f..3ffd7bd370809 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_time_buckets.mdx b/api_docs/kbn_ml_time_buckets.mdx index 6ca7ecacee780..a71518a993b92 100644 --- a/api_docs/kbn_ml_time_buckets.mdx +++ b/api_docs/kbn_ml_time_buckets.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-time-buckets title: "@kbn/ml-time-buckets" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-time-buckets plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-time-buckets'] --- import kbnMlTimeBucketsObj from './kbn_ml_time_buckets.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index 8ebe6d0839fef..3a0432c880c8b 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_ui_actions.mdx b/api_docs/kbn_ml_ui_actions.mdx index 0b5b9ed5aa077..f2105856cceb3 100644 --- a/api_docs/kbn_ml_ui_actions.mdx +++ b/api_docs/kbn_ml_ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-ui-actions title: "@kbn/ml-ui-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-ui-actions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-ui-actions'] --- import kbnMlUiActionsObj from './kbn_ml_ui_actions.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index 6f66f080a991f..6662e6b89afe6 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_ml_validators.mdx b/api_docs/kbn_ml_validators.mdx index 3f30b4cff22b8..667311c58bb89 100644 --- a/api_docs/kbn_ml_validators.mdx +++ b/api_docs/kbn_ml_validators.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-validators title: "@kbn/ml-validators" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-validators plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-validators'] --- import kbnMlValidatorsObj from './kbn_ml_validators.devdocs.json'; diff --git a/api_docs/kbn_mock_idp_utils.mdx b/api_docs/kbn_mock_idp_utils.mdx index c655fda7bcdc4..b0ddbd056b341 100644 --- a/api_docs/kbn_mock_idp_utils.mdx +++ b/api_docs/kbn_mock_idp_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mock-idp-utils title: "@kbn/mock-idp-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mock-idp-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mock-idp-utils'] --- import kbnMockIdpUtilsObj from './kbn_mock_idp_utils.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index a76652b31f692..53926c1c55a34 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index bcafa0c2f87f8..65d5efb93daf5 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; diff --git a/api_docs/kbn_object_versioning_utils.mdx b/api_docs/kbn_object_versioning_utils.mdx index 35496ac7dec18..826a6d8dd2c17 100644 --- a/api_docs/kbn_object_versioning_utils.mdx +++ b/api_docs/kbn_object_versioning_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning-utils title: "@kbn/object-versioning-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning-utils'] --- import kbnObjectVersioningUtilsObj from './kbn_object_versioning_utils.devdocs.json'; diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index b353f50d61067..56e416d7641de 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_observability_alerting_rule_utils.mdx b/api_docs/kbn_observability_alerting_rule_utils.mdx index 110738d4530e3..dde8f5839cbe9 100644 --- a/api_docs/kbn_observability_alerting_rule_utils.mdx +++ b/api_docs/kbn_observability_alerting_rule_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alerting-rule-utils title: "@kbn/observability-alerting-rule-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alerting-rule-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alerting-rule-utils'] --- import kbnObservabilityAlertingRuleUtilsObj from './kbn_observability_alerting_rule_utils.devdocs.json'; diff --git a/api_docs/kbn_observability_alerting_test_data.mdx b/api_docs/kbn_observability_alerting_test_data.mdx index 7b73cf03def52..1d555ed8230f9 100644 --- a/api_docs/kbn_observability_alerting_test_data.mdx +++ b/api_docs/kbn_observability_alerting_test_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alerting-test-data title: "@kbn/observability-alerting-test-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alerting-test-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alerting-test-data'] --- import kbnObservabilityAlertingTestDataObj from './kbn_observability_alerting_test_data.devdocs.json'; diff --git a/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx b/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx index 29c7ff9bc360a..f3380e3aa41eb 100644 --- a/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx +++ b/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-get-padded-alert-time-range-util title: "@kbn/observability-get-padded-alert-time-range-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-get-padded-alert-time-range-util plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-get-padded-alert-time-range-util'] --- import kbnObservabilityGetPaddedAlertTimeRangeUtilObj from './kbn_observability_get_padded_alert_time_range_util.devdocs.json'; diff --git a/api_docs/kbn_observability_logs_overview.mdx b/api_docs/kbn_observability_logs_overview.mdx index 716254b67130c..0a9c546304bda 100644 --- a/api_docs/kbn_observability_logs_overview.mdx +++ b/api_docs/kbn_observability_logs_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-logs-overview title: "@kbn/observability-logs-overview" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-logs-overview plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-logs-overview'] --- import kbnObservabilityLogsOverviewObj from './kbn_observability_logs_overview.devdocs.json'; diff --git a/api_docs/kbn_observability_synthetics_test_data.mdx b/api_docs/kbn_observability_synthetics_test_data.mdx index 00c804f5a59be..5b65c86005640 100644 --- a/api_docs/kbn_observability_synthetics_test_data.mdx +++ b/api_docs/kbn_observability_synthetics_test_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-synthetics-test-data title: "@kbn/observability-synthetics-test-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-synthetics-test-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-synthetics-test-data'] --- import kbnObservabilitySyntheticsTestDataObj from './kbn_observability_synthetics_test_data.devdocs.json'; diff --git a/api_docs/kbn_openapi_bundler.mdx b/api_docs/kbn_openapi_bundler.mdx index a3b31041f8d5e..408b93acb7343 100644 --- a/api_docs/kbn_openapi_bundler.mdx +++ b/api_docs/kbn_openapi_bundler.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-openapi-bundler title: "@kbn/openapi-bundler" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/openapi-bundler plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/openapi-bundler'] --- import kbnOpenapiBundlerObj from './kbn_openapi_bundler.devdocs.json'; diff --git a/api_docs/kbn_openapi_generator.mdx b/api_docs/kbn_openapi_generator.mdx index bfbee2a394d5b..2cb8fea42618f 100644 --- a/api_docs/kbn_openapi_generator.mdx +++ b/api_docs/kbn_openapi_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-openapi-generator title: "@kbn/openapi-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/openapi-generator plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/openapi-generator'] --- import kbnOpenapiGeneratorObj from './kbn_openapi_generator.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index a7642e2dbb0a7..e7bbc53b6b1f8 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index ae2d900614b15..78a27b3faa5d0 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 9e34f64c111c2..51955ddbfa84c 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_panel_loader.mdx b/api_docs/kbn_panel_loader.mdx index 68dfa1d652ba8..cbabc2da6b757 100644 --- a/api_docs/kbn_panel_loader.mdx +++ b/api_docs/kbn_panel_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-panel-loader title: "@kbn/panel-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/panel-loader plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/panel-loader'] --- import kbnPanelLoaderObj from './kbn_panel_loader.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 4d531e84c0a4e..a6a447d772684 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_check.mdx b/api_docs/kbn_plugin_check.mdx index ce6ca411a31f2..e532cb43b974c 100644 --- a/api_docs/kbn_plugin_check.mdx +++ b/api_docs/kbn_plugin_check.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-check title: "@kbn/plugin-check" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-check plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-check'] --- import kbnPluginCheckObj from './kbn_plugin_check.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index f491d9819e1ed..a621e1637afd3 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 31c4fdab59b25..fe2913bec81fb 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_presentation_containers.mdx b/api_docs/kbn_presentation_containers.mdx index 76f53faf4ddee..c5cb58046e3e0 100644 --- a/api_docs/kbn_presentation_containers.mdx +++ b/api_docs/kbn_presentation_containers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-presentation-containers title: "@kbn/presentation-containers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/presentation-containers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/presentation-containers'] --- import kbnPresentationContainersObj from './kbn_presentation_containers.devdocs.json'; diff --git a/api_docs/kbn_presentation_publishing.devdocs.json b/api_docs/kbn_presentation_publishing.devdocs.json index 59b096c6077e7..fef2bc2149a7e 100644 --- a/api_docs/kbn_presentation_publishing.devdocs.json +++ b/api_docs/kbn_presentation_publishing.devdocs.json @@ -930,6 +930,46 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/presentation-publishing", + "id": "def-public.apiPublishesRendered", + "type": "Function", + "tags": [], + "label": "apiPublishesRendered", + "description": [], + "signature": [ + "(unknownApi: unknown) => unknownApi is ", + { + "pluginId": "@kbn/presentation-publishing", + "scope": "public", + "docId": "kibKbnPresentationPublishingPluginApi", + "section": "def-public.PublishesRendered", + "text": "PublishesRendered" + } + ], + "path": "packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/presentation-publishing", + "id": "def-public.apiPublishesRendered.$1", + "type": "Unknown", + "tags": [], + "label": "unknownApi", + "description": [], + "signature": [ + "unknown" + ], + "path": "packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/presentation-publishing", "id": "def-public.apiPublishesSavedObjectId", @@ -6068,6 +6108,184 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/presentation-publishing", + "id": "def-public.PublishesRendered", + "type": "Interface", + "tags": [], + "label": "PublishesRendered", + "description": [], + "path": "packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/presentation-publishing", + "id": "def-public.PublishesRendered.rendered$", + "type": "Object", + "tags": [], + "label": "rendered$", + "description": [], + "signature": [ + "{ source: ", + "Observable", + " | undefined; readonly value: boolean; error: (err: any) => void; forEach: { (next: (value: boolean) => void): Promise; (next: (value: boolean) => void, promiseCtor: PromiseConstructorLike): Promise; }; complete: () => void; getValue: () => boolean; closed: boolean; pipe: { (): ", + "Observable", + "; (op1: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + ", op6: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + ", op6: ", + "OperatorFunction", + ", op7: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + ", op6: ", + "OperatorFunction", + ", op7: ", + "OperatorFunction", + ", op8: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + ", op6: ", + "OperatorFunction", + ", op7: ", + "OperatorFunction", + ", op8: ", + "OperatorFunction", + ", op9: ", + "OperatorFunction", + "): ", + "Observable", + "; (op1: ", + "OperatorFunction", + ", op2: ", + "OperatorFunction", + ", op3: ", + "OperatorFunction", + ", op4: ", + "OperatorFunction", + ", op5: ", + "OperatorFunction", + ", op6: ", + "OperatorFunction", + ", op7: ", + "OperatorFunction", + ", op8: ", + "OperatorFunction", + ", op9: ", + "OperatorFunction", + ", ...operations: ", + "OperatorFunction", + "[]): ", + "Observable", + "; }; operator: ", + "Operator", + " | undefined; lift: (operator: ", + "Operator", + ") => ", + "Observable", + "; subscribe: { (observerOrNext?: Partial<", + "Observer", + "> | ((value: boolean) => void) | undefined): ", + "Subscription", + "; (next?: ((value: boolean) => void) | null | undefined, error?: ((error: any) => void) | null | undefined, complete?: (() => void) | null | undefined): ", + "Subscription", + "; }; toPromise: { (): Promise; (PromiseCtor: PromiseConstructor): Promise; (PromiseCtor: PromiseConstructorLike): Promise; }; observers: ", + "Observer", + "[]; isStopped: boolean; hasError: boolean; thrownError: any; unsubscribe: () => void; readonly observed: boolean; asObservable: () => ", + "Observable", + "; }" + ], + "path": "packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/presentation-publishing", "id": "def-public.PublishesSavedObjectId", diff --git a/api_docs/kbn_presentation_publishing.mdx b/api_docs/kbn_presentation_publishing.mdx index a98c656953afe..24a4bb0388453 100644 --- a/api_docs/kbn_presentation_publishing.mdx +++ b/api_docs/kbn_presentation_publishing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-presentation-publishing title: "@kbn/presentation-publishing" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/presentation-publishing plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/presentation-publishing'] --- import kbnPresentationPublishingObj from './kbn_presentation_publishing.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kib | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 224 | 0 | 188 | 6 | +| 228 | 0 | 192 | 6 | ## Client diff --git a/api_docs/kbn_product_doc_artifact_builder.mdx b/api_docs/kbn_product_doc_artifact_builder.mdx index 4e062192a7409..cb6468a647068 100644 --- a/api_docs/kbn_product_doc_artifact_builder.mdx +++ b/api_docs/kbn_product_doc_artifact_builder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-product-doc-artifact-builder title: "@kbn/product-doc-artifact-builder" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/product-doc-artifact-builder plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/product-doc-artifact-builder'] --- import kbnProductDocArtifactBuilderObj from './kbn_product_doc_artifact_builder.devdocs.json'; diff --git a/api_docs/kbn_product_doc_common.mdx b/api_docs/kbn_product_doc_common.mdx index 87420cf0ec647..cc22add6ac4a7 100644 --- a/api_docs/kbn_product_doc_common.mdx +++ b/api_docs/kbn_product_doc_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-product-doc-common title: "@kbn/product-doc-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/product-doc-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/product-doc-common'] --- import kbnProductDocCommonObj from './kbn_product_doc_common.devdocs.json'; diff --git a/api_docs/kbn_profiling_utils.mdx b/api_docs/kbn_profiling_utils.mdx index 86bbfdebcf800..6c955c46d2720 100644 --- a/api_docs/kbn_profiling_utils.mdx +++ b/api_docs/kbn_profiling_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-profiling-utils title: "@kbn/profiling-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/profiling-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/profiling-utils'] --- import kbnProfilingUtilsObj from './kbn_profiling_utils.devdocs.json'; diff --git a/api_docs/kbn_random_sampling.mdx b/api_docs/kbn_random_sampling.mdx index 5e78dfdfe2b34..e64acc715b608 100644 --- a/api_docs/kbn_random_sampling.mdx +++ b/api_docs/kbn_random_sampling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-random-sampling title: "@kbn/random-sampling" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/random-sampling plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/random-sampling'] --- import kbnRandomSamplingObj from './kbn_random_sampling.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 2f04de9d94755..0e0c3f92862b0 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_react_hooks.mdx b/api_docs/kbn_react_hooks.mdx index 20730bfcd0c09..3e2055792400e 100644 --- a/api_docs/kbn_react_hooks.mdx +++ b/api_docs/kbn_react_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-hooks title: "@kbn/react-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-hooks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-hooks'] --- import kbnReactHooksObj from './kbn_react_hooks.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_common.mdx b/api_docs/kbn_react_kibana_context_common.mdx index 6cd0690a0226a..44cf43856620a 100644 --- a/api_docs/kbn_react_kibana_context_common.mdx +++ b/api_docs/kbn_react_kibana_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-common title: "@kbn/react-kibana-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-common'] --- import kbnReactKibanaContextCommonObj from './kbn_react_kibana_context_common.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_render.mdx b/api_docs/kbn_react_kibana_context_render.mdx index 4b5ee413441aa..aaa22aa533133 100644 --- a/api_docs/kbn_react_kibana_context_render.mdx +++ b/api_docs/kbn_react_kibana_context_render.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-render title: "@kbn/react-kibana-context-render" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-render plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-render'] --- import kbnReactKibanaContextRenderObj from './kbn_react_kibana_context_render.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_root.mdx b/api_docs/kbn_react_kibana_context_root.mdx index 754855fb36a55..fab748b704898 100644 --- a/api_docs/kbn_react_kibana_context_root.mdx +++ b/api_docs/kbn_react_kibana_context_root.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-root title: "@kbn/react-kibana-context-root" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-root plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-root'] --- import kbnReactKibanaContextRootObj from './kbn_react_kibana_context_root.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_styled.mdx b/api_docs/kbn_react_kibana_context_styled.mdx index c6c54a1d5b1d3..989800844a429 100644 --- a/api_docs/kbn_react_kibana_context_styled.mdx +++ b/api_docs/kbn_react_kibana_context_styled.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-styled title: "@kbn/react-kibana-context-styled" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-styled plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-styled'] --- import kbnReactKibanaContextStyledObj from './kbn_react_kibana_context_styled.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_theme.mdx b/api_docs/kbn_react_kibana_context_theme.mdx index e44baaccadf56..f49c2cde94097 100644 --- a/api_docs/kbn_react_kibana_context_theme.mdx +++ b/api_docs/kbn_react_kibana_context_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-theme title: "@kbn/react-kibana-context-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-theme plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-theme'] --- import kbnReactKibanaContextThemeObj from './kbn_react_kibana_context_theme.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_mount.mdx b/api_docs/kbn_react_kibana_mount.mdx index 51f325742ccd4..bf3050ab859ce 100644 --- a/api_docs/kbn_react_kibana_mount.mdx +++ b/api_docs/kbn_react_kibana_mount.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-mount title: "@kbn/react-kibana-mount" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-mount plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-mount'] --- import kbnReactKibanaMountObj from './kbn_react_kibana_mount.devdocs.json'; diff --git a/api_docs/kbn_recently_accessed.mdx b/api_docs/kbn_recently_accessed.mdx index 4a846cc75a52f..4940bb999e106 100644 --- a/api_docs/kbn_recently_accessed.mdx +++ b/api_docs/kbn_recently_accessed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-recently-accessed title: "@kbn/recently-accessed" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/recently-accessed plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/recently-accessed'] --- import kbnRecentlyAccessedObj from './kbn_recently_accessed.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index 974d732be038d..9e98e6924effb 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index 30aa9b8910a50..b8cd9d852638a 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index 4cdfaae3a8f60..40bb77a397c06 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.devdocs.json b/api_docs/kbn_repo_source_classifier.devdocs.json index 0d96329a321b7..05f9cc5a83239 100644 --- a/api_docs/kbn_repo_source_classifier.devdocs.json +++ b/api_docs/kbn_repo_source_classifier.devdocs.json @@ -76,7 +76,13 @@ "description": [], "signature": [ "(absolute: string) => ", - "ModuleId" + { + "pluginId": "@kbn/repo-source-classifier", + "scope": "common", + "docId": "kibKbnRepoSourceClassifierPluginApi", + "section": "def-common.ModuleId", + "text": "ModuleId" + } ], "path": "packages/kbn-repo-source-classifier/src/repo_source_classifier.ts", "deprecated": false, @@ -105,7 +111,133 @@ } ], "functions": [], - "interfaces": [], + "interfaces": [ + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId", + "type": "Interface", + "tags": [], + "label": "ModuleId", + "description": [], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.type", + "type": "CompoundType", + "tags": [], + "label": "type", + "description": [ + "Type of the module" + ], + "signature": [ + "\"non-package\" | \"tests or mocks\" | \"static\" | \"tooling\" | \"server package\" | \"browser package\" | \"common package\"" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.group", + "type": "CompoundType", + "tags": [], + "label": "group", + "description": [ + "Specifies the group to which this module belongs" + ], + "signature": [ + "\"search\" | \"security\" | \"observability\" | \"platform\" | \"common\"" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.visibility", + "type": "CompoundType", + "tags": [], + "label": "visibility", + "description": [ + "Specifies the module visibility, i.e. whether it can be accessed by everybody or only modules in the same group" + ], + "signature": [ + "\"private\" | \"shared\"" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.repoRel", + "type": "string", + "tags": [], + "label": "repoRel", + "description": [ + "repo relative path to the module's source file" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.pkgInfo", + "type": "Object", + "tags": [], + "label": "pkgInfo", + "description": [ + "info about the package the source file is within, in the case the file is found within a package" + ], + "signature": [ + "PkgInfo", + " | undefined" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.manifest", + "type": "CompoundType", + "tags": [], + "label": "manifest", + "description": [ + "The type of package, as described in the manifest" + ], + "signature": [ + "KibanaPackageManifest", + " | undefined" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/repo-source-classifier", + "id": "def-common.ModuleId.dirs", + "type": "Array", + "tags": [], + "label": "dirs", + "description": [ + "path segments of the dirname of this" + ], + "signature": [ + "string[]" + ], + "path": "packages/kbn-repo-source-classifier/src/module_id.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], "enums": [], "misc": [ { diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index e058f3289b521..8604d0e30b56a 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; @@ -21,13 +21,16 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 6 | 0 | 6 | 1 | +| 14 | 0 | 7 | 1 | ## Common ### Classes +### Interfaces + + ### Consts, variables and types diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 7df0a1957e2eb..d8c9725139d0e 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_csv_share_panel.mdx b/api_docs/kbn_reporting_csv_share_panel.mdx index e55c53489e813..2a36aa5e07ef6 100644 --- a/api_docs/kbn_reporting_csv_share_panel.mdx +++ b/api_docs/kbn_reporting_csv_share_panel.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-csv-share-panel title: "@kbn/reporting-csv-share-panel" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-csv-share-panel plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-csv-share-panel'] --- import kbnReportingCsvSharePanelObj from './kbn_reporting_csv_share_panel.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_csv.devdocs.json b/api_docs/kbn_reporting_export_types_csv.devdocs.json index cab900fc5f388..9e4bdaccb78ca 100644 --- a/api_docs/kbn_reporting_export_types_csv.devdocs.json +++ b/api_docs/kbn_reporting_export_types_csv.devdocs.json @@ -168,7 +168,7 @@ "section": "def-server.CoreSetup", "text": "CoreSetup" }, - ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -192,7 +192,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -565,7 +565,7 @@ "section": "def-server.CoreSetup", "text": "CoreSetup" }, - ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -589,7 +589,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_reporting_export_types_csv.mdx b/api_docs/kbn_reporting_export_types_csv.mdx index 304dfce60f471..3db1848c9823d 100644 --- a/api_docs/kbn_reporting_export_types_csv.mdx +++ b/api_docs/kbn_reporting_export_types_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-csv title: "@kbn/reporting-export-types-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-csv plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-csv'] --- import kbnReportingExportTypesCsvObj from './kbn_reporting_export_types_csv.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_csv_common.mdx b/api_docs/kbn_reporting_export_types_csv_common.mdx index ac431b442f31e..cdcfec2e06e92 100644 --- a/api_docs/kbn_reporting_export_types_csv_common.mdx +++ b/api_docs/kbn_reporting_export_types_csv_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-csv-common title: "@kbn/reporting-export-types-csv-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-csv-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-csv-common'] --- import kbnReportingExportTypesCsvCommonObj from './kbn_reporting_export_types_csv_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_pdf.devdocs.json b/api_docs/kbn_reporting_export_types_pdf.devdocs.json index 9e06d9c81052f..f0fb7f203f946 100644 --- a/api_docs/kbn_reporting_export_types_pdf.devdocs.json +++ b/api_docs/kbn_reporting_export_types_pdf.devdocs.json @@ -176,7 +176,7 @@ "section": "def-server.CoreSetup", "text": "CoreSetup" }, - ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -200,7 +200,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -597,7 +597,7 @@ "section": "def-server.CoreSetup", "text": "CoreSetup" }, - ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -621,7 +621,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_reporting_export_types_pdf.mdx b/api_docs/kbn_reporting_export_types_pdf.mdx index 2fb6e8f28c6e6..b158e11ec5297 100644 --- a/api_docs/kbn_reporting_export_types_pdf.mdx +++ b/api_docs/kbn_reporting_export_types_pdf.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-pdf title: "@kbn/reporting-export-types-pdf" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-pdf plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-pdf'] --- import kbnReportingExportTypesPdfObj from './kbn_reporting_export_types_pdf.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_pdf_common.mdx b/api_docs/kbn_reporting_export_types_pdf_common.mdx index 94093ebe7b974..61fff7e6e9d3f 100644 --- a/api_docs/kbn_reporting_export_types_pdf_common.mdx +++ b/api_docs/kbn_reporting_export_types_pdf_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-pdf-common title: "@kbn/reporting-export-types-pdf-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-pdf-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-pdf-common'] --- import kbnReportingExportTypesPdfCommonObj from './kbn_reporting_export_types_pdf_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_png.devdocs.json b/api_docs/kbn_reporting_export_types_png.devdocs.json index 7553df09eef8c..99fb9ba2966b3 100644 --- a/api_docs/kbn_reporting_export_types_png.devdocs.json +++ b/api_docs/kbn_reporting_export_types_png.devdocs.json @@ -176,7 +176,7 @@ "section": "def-server.CoreSetup", "text": "CoreSetup" }, - ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + ", config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -200,7 +200,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_reporting_export_types_png.mdx b/api_docs/kbn_reporting_export_types_png.mdx index d0fe624eec46d..09f70232100af 100644 --- a/api_docs/kbn_reporting_export_types_png.mdx +++ b/api_docs/kbn_reporting_export_types_png.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-png title: "@kbn/reporting-export-types-png" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-png plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-png'] --- import kbnReportingExportTypesPngObj from './kbn_reporting_export_types_png.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_png_common.mdx b/api_docs/kbn_reporting_export_types_png_common.mdx index e24ecc2174886..0cf4c8e6e4514 100644 --- a/api_docs/kbn_reporting_export_types_png_common.mdx +++ b/api_docs/kbn_reporting_export_types_png_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-png-common title: "@kbn/reporting-export-types-png-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-png-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-png-common'] --- import kbnReportingExportTypesPngCommonObj from './kbn_reporting_export_types_png_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_mocks_server.devdocs.json b/api_docs/kbn_reporting_mocks_server.devdocs.json index 043566a99e0ea..2ad48d68483b0 100644 --- a/api_docs/kbn_reporting_mocks_server.devdocs.json +++ b/api_docs/kbn_reporting_mocks_server.devdocs.json @@ -29,7 +29,7 @@ "signature": [ "(overrides?: ", "_DeepPartialObject", - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -37,7 +37,7 @@ "section": "def-common.ByteSizeValue", "text": "ByteSizeValue" }, - "; useByteOrderMarkEncoding: boolean; maxConcurrentShardRequests: number; }>; capture: Readonly<{} & { maxAttempts: number; }>; roles: Readonly<{} & { enabled: boolean; allow: string[]; }>; kibanaServer: Readonly<{ hostname?: string | undefined; protocol?: string | undefined; port?: number | undefined; } & {}>; queue: Readonly<{} & { timeout: number | moment.Duration; pollInterval: number | moment.Duration; indexInterval: string; pollEnabled: boolean; pollIntervalErrorMultiplier: number; }>; poll: Readonly<{} & { jobCompletionNotifier: Readonly<{} & { interval: number; intervalErrorMultiplier: number; }>; jobsRefresh: Readonly<{} & { interval: number; intervalErrorMultiplier: number; }>; }>; export_types: Readonly<{} & { csv: Readonly<{} & { enabled: boolean; }>; png: Readonly<{} & { enabled: boolean; }>; pdf: Readonly<{} & { enabled: boolean; }>; }>; statefulSettings: Readonly<{} & { enabled: boolean; }>; }>>) => Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; useByteOrderMarkEncoding: boolean; maxConcurrentShardRequests: number; }>; capture: Readonly<{} & { maxAttempts: number; }>; roles: Readonly<{} & { enabled: boolean; allow: string[]; }>; kibanaServer: Readonly<{ hostname?: string | undefined; protocol?: string | undefined; port?: number | undefined; } & {}>; queue: Readonly<{} & { timeout: number | moment.Duration; pollInterval: number | moment.Duration; indexInterval: string; pollEnabled: boolean; pollIntervalErrorMultiplier: number; }>; poll: Readonly<{} & { jobCompletionNotifier: Readonly<{} & { interval: number; intervalErrorMultiplier: number; }>; jobsRefresh: Readonly<{} & { interval: number; intervalErrorMultiplier: number; }>; }>; export_types: Readonly<{} & { csv: Readonly<{} & { enabled: boolean; }>; png: Readonly<{} & { enabled: boolean; }>; pdf: Readonly<{} & { enabled: boolean; }>; }>; statefulSettings: Readonly<{} & { enabled: boolean; }>; }>>) => Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -60,7 +60,7 @@ "description": [], "signature": [ "_DeepPartialObject", - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_reporting_mocks_server.mdx b/api_docs/kbn_reporting_mocks_server.mdx index 7800bc83c9d28..a33d0e142cfe9 100644 --- a/api_docs/kbn_reporting_mocks_server.mdx +++ b/api_docs/kbn_reporting_mocks_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-mocks-server title: "@kbn/reporting-mocks-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-mocks-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-mocks-server'] --- import kbnReportingMocksServerObj from './kbn_reporting_mocks_server.devdocs.json'; diff --git a/api_docs/kbn_reporting_public.mdx b/api_docs/kbn_reporting_public.mdx index b2c81f42f8b48..cc9fbdb846d21 100644 --- a/api_docs/kbn_reporting_public.mdx +++ b/api_docs/kbn_reporting_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-public title: "@kbn/reporting-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-public plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-public'] --- import kbnReportingPublicObj from './kbn_reporting_public.devdocs.json'; diff --git a/api_docs/kbn_reporting_server.devdocs.json b/api_docs/kbn_reporting_server.devdocs.json index 2edf31cbaf9e4..29373857cb9db 100644 --- a/api_docs/kbn_reporting_server.devdocs.json +++ b/api_docs/kbn_reporting_server.devdocs.json @@ -416,7 +416,7 @@ "label": "config", "description": [], "signature": [ - "Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -467,7 +467,7 @@ "section": "def-server.PluginInitializerContext", "text": "PluginInitializerContext" }, - "; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -949,7 +949,7 @@ "label": "getFullRedirectAppUrl", "description": [], "signature": [ - "(config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "(config: Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -973,7 +973,7 @@ "label": "config", "description": [], "signature": [ - "Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "Readonly<{ encryptionKey?: string | undefined; } & { enabled: boolean; csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -1661,7 +1661,7 @@ "label": "ReportingConfigType", "description": [], "signature": [ - "{ readonly encryptionKey?: string | undefined; readonly enabled: boolean; readonly csv: Readonly<{} & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; enablePanelActionDownload: boolean; maxSizeBytes: number | ", + "{ readonly encryptionKey?: string | undefined; readonly enabled: boolean; readonly csv: Readonly<{ enablePanelActionDownload?: boolean | undefined; } & { scroll: Readonly<{} & { size: number; duration: string; strategy: \"scroll\" | \"pit\"; }>; checkForFormulas: boolean; escapeFormulaValues: boolean; maxSizeBytes: number | ", { "pluginId": "@kbn/config-schema", "scope": "common", @@ -1949,7 +1949,7 @@ "section": "def-common.Type", "text": "Type" }, - "; maxSizeBytes: ", + "; maxSizeBytes: ", { "pluginId": "@kbn/config-schema", "scope": "common", diff --git a/api_docs/kbn_reporting_server.mdx b/api_docs/kbn_reporting_server.mdx index 4e74a76505d62..d568f4981c682 100644 --- a/api_docs/kbn_reporting_server.mdx +++ b/api_docs/kbn_reporting_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-server title: "@kbn/reporting-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-server'] --- import kbnReportingServerObj from './kbn_reporting_server.devdocs.json'; diff --git a/api_docs/kbn_resizable_layout.mdx b/api_docs/kbn_resizable_layout.mdx index 002eab56e8277..a0ed515f89fa6 100644 --- a/api_docs/kbn_resizable_layout.mdx +++ b/api_docs/kbn_resizable_layout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-resizable-layout title: "@kbn/resizable-layout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/resizable-layout plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/resizable-layout'] --- import kbnResizableLayoutObj from './kbn_resizable_layout.devdocs.json'; diff --git a/api_docs/kbn_response_ops_feature_flag_service.mdx b/api_docs/kbn_response_ops_feature_flag_service.mdx index d777149cd330e..76db300e5b1f6 100644 --- a/api_docs/kbn_response_ops_feature_flag_service.mdx +++ b/api_docs/kbn_response_ops_feature_flag_service.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-response-ops-feature-flag-service title: "@kbn/response-ops-feature-flag-service" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/response-ops-feature-flag-service plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/response-ops-feature-flag-service'] --- import kbnResponseOpsFeatureFlagServiceObj from './kbn_response_ops_feature_flag_service.devdocs.json'; diff --git a/api_docs/kbn_response_ops_rule_params.mdx b/api_docs/kbn_response_ops_rule_params.mdx index f0e59e494c8b5..37d9384669a34 100644 --- a/api_docs/kbn_response_ops_rule_params.mdx +++ b/api_docs/kbn_response_ops_rule_params.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-response-ops-rule-params title: "@kbn/response-ops-rule-params" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/response-ops-rule-params plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/response-ops-rule-params'] --- import kbnResponseOpsRuleParamsObj from './kbn_response_ops_rule_params.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index af96ca9b3820e..961d1403360c2 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rollup.mdx b/api_docs/kbn_rollup.mdx index 17dc5651ef5a5..e1c003215906e 100644 --- a/api_docs/kbn_rollup.mdx +++ b/api_docs/kbn_rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rollup title: "@kbn/rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rollup plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rollup'] --- import kbnRollupObj from './kbn_rollup.devdocs.json'; diff --git a/api_docs/kbn_router_to_openapispec.mdx b/api_docs/kbn_router_to_openapispec.mdx index 870d1ef869b59..0ce7b1d434468 100644 --- a/api_docs/kbn_router_to_openapispec.mdx +++ b/api_docs/kbn_router_to_openapispec.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-router-to-openapispec title: "@kbn/router-to-openapispec" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/router-to-openapispec plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/router-to-openapispec'] --- import kbnRouterToOpenapispecObj from './kbn_router_to_openapispec.devdocs.json'; diff --git a/api_docs/kbn_router_utils.mdx b/api_docs/kbn_router_utils.mdx index 9580140373345..f73bc3665d78f 100644 --- a/api_docs/kbn_router_utils.mdx +++ b/api_docs/kbn_router_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-router-utils title: "@kbn/router-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/router-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/router-utils'] --- import kbnRouterUtilsObj from './kbn_router_utils.devdocs.json'; diff --git a/api_docs/kbn_rrule.mdx b/api_docs/kbn_rrule.mdx index 9e2cea991b593..e7ae23fb5cd25 100644 --- a/api_docs/kbn_rrule.mdx +++ b/api_docs/kbn_rrule.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rrule title: "@kbn/rrule" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rrule plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rrule'] --- import kbnRruleObj from './kbn_rrule.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index f7d51280be1aa..a7bf92317abc8 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index 09a29804d2def..c82f7fc78e847 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_screenshotting_server.mdx b/api_docs/kbn_screenshotting_server.mdx index 3bfec87796410..2648777ed4d17 100644 --- a/api_docs/kbn_screenshotting_server.mdx +++ b/api_docs/kbn_screenshotting_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-screenshotting-server title: "@kbn/screenshotting-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/screenshotting-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/screenshotting-server'] --- import kbnScreenshottingServerObj from './kbn_screenshotting_server.devdocs.json'; diff --git a/api_docs/kbn_search_api_keys_components.mdx b/api_docs/kbn_search_api_keys_components.mdx index a6048f1d32374..252c311b2585a 100644 --- a/api_docs/kbn_search_api_keys_components.mdx +++ b/api_docs/kbn_search_api_keys_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-api-keys-components title: "@kbn/search-api-keys-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-api-keys-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-keys-components'] --- import kbnSearchApiKeysComponentsObj from './kbn_search_api_keys_components.devdocs.json'; diff --git a/api_docs/kbn_search_api_keys_server.mdx b/api_docs/kbn_search_api_keys_server.mdx index 553ec485329a3..12f848f054b3d 100644 --- a/api_docs/kbn_search_api_keys_server.mdx +++ b/api_docs/kbn_search_api_keys_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-api-keys-server title: "@kbn/search-api-keys-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-api-keys-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-keys-server'] --- import kbnSearchApiKeysServerObj from './kbn_search_api_keys_server.devdocs.json'; diff --git a/api_docs/kbn_search_api_panels.mdx b/api_docs/kbn_search_api_panels.mdx index 104d624a4d2ac..85b296ba8c93e 100644 --- a/api_docs/kbn_search_api_panels.mdx +++ b/api_docs/kbn_search_api_panels.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-api-panels title: "@kbn/search-api-panels" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-api-panels plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-panels'] --- import kbnSearchApiPanelsObj from './kbn_search_api_panels.devdocs.json'; diff --git a/api_docs/kbn_search_connectors.mdx b/api_docs/kbn_search_connectors.mdx index 7924fda854d7f..bcd0893e3c954 100644 --- a/api_docs/kbn_search_connectors.mdx +++ b/api_docs/kbn_search_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-connectors title: "@kbn/search-connectors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-connectors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-connectors'] --- import kbnSearchConnectorsObj from './kbn_search_connectors.devdocs.json'; diff --git a/api_docs/kbn_search_errors.mdx b/api_docs/kbn_search_errors.mdx index dc45d6dea21f8..34b04e49892fa 100644 --- a/api_docs/kbn_search_errors.mdx +++ b/api_docs/kbn_search_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-errors title: "@kbn/search-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-errors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-errors'] --- import kbnSearchErrorsObj from './kbn_search_errors.devdocs.json'; diff --git a/api_docs/kbn_search_index_documents.mdx b/api_docs/kbn_search_index_documents.mdx index 83b39f59a7ba0..241b13a85419f 100644 --- a/api_docs/kbn_search_index_documents.mdx +++ b/api_docs/kbn_search_index_documents.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-index-documents title: "@kbn/search-index-documents" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-index-documents plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-index-documents'] --- import kbnSearchIndexDocumentsObj from './kbn_search_index_documents.devdocs.json'; diff --git a/api_docs/kbn_search_response_warnings.mdx b/api_docs/kbn_search_response_warnings.mdx index 7a8efa3f35550..1bf051442a33d 100644 --- a/api_docs/kbn_search_response_warnings.mdx +++ b/api_docs/kbn_search_response_warnings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-response-warnings title: "@kbn/search-response-warnings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-response-warnings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-response-warnings'] --- import kbnSearchResponseWarningsObj from './kbn_search_response_warnings.devdocs.json'; diff --git a/api_docs/kbn_search_shared_ui.mdx b/api_docs/kbn_search_shared_ui.mdx index 9b4aed1b3dbcc..01a9b14b956b2 100644 --- a/api_docs/kbn_search_shared_ui.mdx +++ b/api_docs/kbn_search_shared_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-shared-ui title: "@kbn/search-shared-ui" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-shared-ui plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-shared-ui'] --- import kbnSearchSharedUiObj from './kbn_search_shared_ui.devdocs.json'; diff --git a/api_docs/kbn_search_types.mdx b/api_docs/kbn_search_types.mdx index 178df97d36042..de184274e37ba 100644 --- a/api_docs/kbn_search_types.mdx +++ b/api_docs/kbn_search_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-types title: "@kbn/search-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-types'] --- import kbnSearchTypesObj from './kbn_search_types.devdocs.json'; diff --git a/api_docs/kbn_security_api_key_management.mdx b/api_docs/kbn_security_api_key_management.mdx index 55b3058aba268..9b98d97b9f4dc 100644 --- a/api_docs/kbn_security_api_key_management.mdx +++ b/api_docs/kbn_security_api_key_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-api-key-management title: "@kbn/security-api-key-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-api-key-management plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-api-key-management'] --- import kbnSecurityApiKeyManagementObj from './kbn_security_api_key_management.devdocs.json'; diff --git a/api_docs/kbn_security_authorization_core.mdx b/api_docs/kbn_security_authorization_core.mdx index 1d17f2d087be2..eceb53a89e3b8 100644 --- a/api_docs/kbn_security_authorization_core.mdx +++ b/api_docs/kbn_security_authorization_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-authorization-core title: "@kbn/security-authorization-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-authorization-core plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-authorization-core'] --- import kbnSecurityAuthorizationCoreObj from './kbn_security_authorization_core.devdocs.json'; diff --git a/api_docs/kbn_security_authorization_core_common.mdx b/api_docs/kbn_security_authorization_core_common.mdx index bff499fa9345a..31212cad0aec2 100644 --- a/api_docs/kbn_security_authorization_core_common.mdx +++ b/api_docs/kbn_security_authorization_core_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-authorization-core-common title: "@kbn/security-authorization-core-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-authorization-core-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-authorization-core-common'] --- import kbnSecurityAuthorizationCoreCommonObj from './kbn_security_authorization_core_common.devdocs.json'; diff --git a/api_docs/kbn_security_form_components.mdx b/api_docs/kbn_security_form_components.mdx index 78f402c754e02..5041a0a85706c 100644 --- a/api_docs/kbn_security_form_components.mdx +++ b/api_docs/kbn_security_form_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-form-components title: "@kbn/security-form-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-form-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-form-components'] --- import kbnSecurityFormComponentsObj from './kbn_security_form_components.devdocs.json'; diff --git a/api_docs/kbn_security_hardening.mdx b/api_docs/kbn_security_hardening.mdx index 5fdea6e063689..c34042683f479 100644 --- a/api_docs/kbn_security_hardening.mdx +++ b/api_docs/kbn_security_hardening.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-hardening title: "@kbn/security-hardening" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-hardening plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-hardening'] --- import kbnSecurityHardeningObj from './kbn_security_hardening.devdocs.json'; diff --git a/api_docs/kbn_security_plugin_types_common.mdx b/api_docs/kbn_security_plugin_types_common.mdx index 7e87f9068bb7e..da80a699e6d6a 100644 --- a/api_docs/kbn_security_plugin_types_common.mdx +++ b/api_docs/kbn_security_plugin_types_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-common title: "@kbn/security-plugin-types-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-common plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-common'] --- import kbnSecurityPluginTypesCommonObj from './kbn_security_plugin_types_common.devdocs.json'; diff --git a/api_docs/kbn_security_plugin_types_public.mdx b/api_docs/kbn_security_plugin_types_public.mdx index 50fe01f23aeb9..251647d177a74 100644 --- a/api_docs/kbn_security_plugin_types_public.mdx +++ b/api_docs/kbn_security_plugin_types_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-public title: "@kbn/security-plugin-types-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-public plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-public'] --- import kbnSecurityPluginTypesPublicObj from './kbn_security_plugin_types_public.devdocs.json'; diff --git a/api_docs/kbn_security_plugin_types_server.mdx b/api_docs/kbn_security_plugin_types_server.mdx index f27bffd19872e..300152e0bb729 100644 --- a/api_docs/kbn_security_plugin_types_server.mdx +++ b/api_docs/kbn_security_plugin_types_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-server title: "@kbn/security-plugin-types-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-server'] --- import kbnSecurityPluginTypesServerObj from './kbn_security_plugin_types_server.devdocs.json'; diff --git a/api_docs/kbn_security_role_management_model.mdx b/api_docs/kbn_security_role_management_model.mdx index 7e0dda8f661b3..4c9d3f2b67ee6 100644 --- a/api_docs/kbn_security_role_management_model.mdx +++ b/api_docs/kbn_security_role_management_model.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-role-management-model title: "@kbn/security-role-management-model" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-role-management-model plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-role-management-model'] --- import kbnSecurityRoleManagementModelObj from './kbn_security_role_management_model.devdocs.json'; diff --git a/api_docs/kbn_security_solution_distribution_bar.mdx b/api_docs/kbn_security_solution_distribution_bar.mdx index 99d8879d9858d..25da651b8271e 100644 --- a/api_docs/kbn_security_solution_distribution_bar.mdx +++ b/api_docs/kbn_security_solution_distribution_bar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-distribution-bar title: "@kbn/security-solution-distribution-bar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-distribution-bar plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-distribution-bar'] --- import kbnSecuritySolutionDistributionBarObj from './kbn_security_solution_distribution_bar.devdocs.json'; diff --git a/api_docs/kbn_security_solution_features.mdx b/api_docs/kbn_security_solution_features.mdx index 4dc2e1497ec13..66476323413bd 100644 --- a/api_docs/kbn_security_solution_features.mdx +++ b/api_docs/kbn_security_solution_features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-features title: "@kbn/security-solution-features" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-features plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-features'] --- import kbnSecuritySolutionFeaturesObj from './kbn_security_solution_features.devdocs.json'; diff --git a/api_docs/kbn_security_solution_navigation.mdx b/api_docs/kbn_security_solution_navigation.mdx index 29b2e83f6ca2b..000b86f086aba 100644 --- a/api_docs/kbn_security_solution_navigation.mdx +++ b/api_docs/kbn_security_solution_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-navigation title: "@kbn/security-solution-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-navigation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-navigation'] --- import kbnSecuritySolutionNavigationObj from './kbn_security_solution_navigation.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index cfe0f246f27b5..2b59e0892a47f 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index 9b1d2c09bbd1f..942e93d70f4cb 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_security_ui_components.mdx b/api_docs/kbn_security_ui_components.mdx index 6ae7a98a47739..e9e91ffa77ea6 100644 --- a/api_docs/kbn_security_ui_components.mdx +++ b/api_docs/kbn_security_ui_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-ui-components title: "@kbn/security-ui-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-ui-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-ui-components'] --- import kbnSecurityUiComponentsObj from './kbn_security_ui_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 47d9a5051ce68..99371fca4450b 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index c1afd0809f45c..bbef05d204664 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 3aabc7cd739f5..67f457a110596 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 58fed3c2bc90a..9620ade2aaec0 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 2932db77d5b76..6742cd81d2441 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 6d347fbdfffff..a1768c42be7b6 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 523fa351e2754..70c1fb0109433 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 8f29dc09e7cf2..306ebd271f348 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index bc746058c2d1b..79cda27e5917a 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 150d70f74d87b..3d5caaa549f76 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 69e1df6de9c37..ce14082229676 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 8d97bf4002205..072ea012d8583 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index ac34362ddd911..9c90b07f00b30 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 4dd48414d4e7e..f4e420b4a3c24 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index a4966c61a76fc..f866cebd99e38 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 2a87cc0882f05..ea6b11f36e56f 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.devdocs.json b/api_docs/kbn_securitysolution_utils.devdocs.json index 7dbb4b59056c6..d799309b1821d 100644 --- a/api_docs/kbn_securitysolution_utils.devdocs.json +++ b/api_docs/kbn_securitysolution_utils.devdocs.json @@ -242,6 +242,60 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-utils", + "id": "def-common.debounceAsync", + "type": "Function", + "tags": [], + "label": "debounceAsync", + "description": [ + "\nUnlike lodash's debounce, which resolves intermediate calls with the most\nrecent value, this implementation waits to resolve intermediate calls until\nthe next invocation resolves.\n" + ], + "signature": [ + "(fn: (...args: Args) => Result, intervalMs: number) => (...args: Args) => Promise>" + ], + "path": "packages/kbn-securitysolution-utils/src/debounce_async/debounce_async.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-utils", + "id": "def-common.debounceAsync.$1", + "type": "Function", + "tags": [], + "label": "fn", + "description": [ + "an async function" + ], + "signature": [ + "(...args: Args) => Result" + ], + "path": "packages/kbn-securitysolution-utils/src/debounce_async/debounce_async.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/securitysolution-utils", + "id": "def-common.debounceAsync.$2", + "type": "number", + "tags": [], + "label": "intervalMs", + "description": [], + "signature": [ + "number" + ], + "path": "packages/kbn-securitysolution-utils/src/debounce_async/debounce_async.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [ + "A debounced async function that resolves on the next invocation" + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-utils", "id": "def-common.getIndexListFromEsqlQuery", diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 1d682e7bdd98c..684e99e1216cb 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-detection-engine](https://github.com/orgs/elastic/tea | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 60 | 0 | 54 | 0 | +| 63 | 0 | 55 | 0 | ## Common diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index cba4987e0efda..14ba1d76b7845 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.devdocs.json b/api_docs/kbn_server_route_repository.devdocs.json index 274e388eb0545..b402826a9d604 100644 --- a/api_docs/kbn_server_route_repository.devdocs.json +++ b/api_docs/kbn_server_route_repository.devdocs.json @@ -88,7 +88,15 @@ "label": "formatRequest", "description": [], "signature": [ - "(endpoint: string, pathParams: Record) => { method: Method; pathname: string; version: string; }" + "(endpoint: string, pathParams: Record) => { method: \"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + "; pathname: string; version: string; }" ], "path": "packages/kbn-server-route-repository-utils/src/format_request.ts", "deprecated": false, @@ -136,7 +144,15 @@ "label": "parseEndpoint", "description": [], "signature": [ - "(endpoint: string) => { method: Method; pathname: string; version: string; }" + "(endpoint: string) => { method: \"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + "; pathname: string; version: string; }" ], "path": "packages/kbn-server-route-repository-utils/src/parse_endpoint.ts", "deprecated": false, @@ -185,15 +201,15 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - ">; logger: ", + " | undefined, any, any, any>>; logger: ", { "pluginId": "@kbn/logging", "scope": "common", @@ -255,15 +271,15 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "; }" + " | undefined, any, any, any>; }" ], "path": "packages/kbn-server-route-repository/src/register_routes.ts", "deprecated": false, @@ -344,49 +360,6 @@ } ], "interfaces": [ - { - "parentPluginId": "@kbn/server-route-repository", - "id": "def-server.DefaultRouteCreateOptions", - "type": "Interface", - "tags": [], - "label": "DefaultRouteCreateOptions", - "description": [], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/server-route-repository", - "id": "def-server.DefaultRouteCreateOptions.options", - "type": "Object", - "tags": [], - "label": "options", - "description": [], - "signature": [ - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.RouteConfigOptions", - "text": "RouteConfigOptions" - }, - "<", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.RouteMethod", - "text": "RouteMethod" - }, - "> | undefined" - ], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "@kbn/server-route-repository", "id": "def-server.DefaultRouteHandlerResources", @@ -468,7 +441,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => Promise<", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => Promise<", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -513,7 +494,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -539,7 +528,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => ", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -588,7 +585,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -600,34 +605,6 @@ } ], "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/server-route-repository", - "id": "def-server.RouteState", - "type": "Interface", - "tags": [], - "label": "RouteState", - "description": [], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/server-route-repository", - "id": "def-server.RouteState.Unnamed", - "type": "IndexSignature", - "tags": [], - "label": "[endpoint: string]: any", - "description": [], - "signature": [ - "[endpoint: string]: any" - ], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false } ], "enums": [], @@ -664,7 +641,7 @@ "section": "def-common.ServerRouteCreateOptions", "text": "ServerRouteCreateOptions" }, - "> ? TRouteParamsRT extends ", + " | undefined> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -703,15 +680,7 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " | undefined, any, any, ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.ServerRouteCreateOptions", - "text": "ServerRouteCreateOptions" - }, - "> ? TRouteParamsRT extends ", + " | undefined, any, any, any> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -726,6 +695,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/server-route-repository", + "id": "def-server.DefaultRouteCreateOptions", + "type": "Type", + "tags": [], + "label": "DefaultRouteCreateOptions", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteConfigOptions", + "text": "RouteConfigOptions" + }, + "<\"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + ">" + ], + "path": "packages/kbn-server-route-repository-utils/src/typings.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/server-route-repository", "id": "def-server.EndpointOf", @@ -782,15 +781,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - " ? TReturnType extends ", + " ? TReturnType extends ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -852,7 +843,15 @@ "label": "ServerRoute", "description": [], "signature": [ - "{ endpoint: TEndpoint; handler: ServerRouteHandler; } & TRouteCreateOptions & (TRouteParamsRT extends ", + "{ endpoint: TEndpoint; handler: ServerRouteHandler; security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; } & (TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -860,7 +859,15 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " ? { params: TRouteParamsRT; } : {})" + " ? { params: TRouteParamsRT; } : {}) & (TRouteCreateOptions extends ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.DefaultRouteCreateOptions", + "text": "DefaultRouteCreateOptions" + }, + " ? { options: TRouteCreateOptions; } : {})" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -891,7 +898,15 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " | undefined, any, any, Record>; }" + " | undefined, any, any, ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRouteCreateOptions", + "text": "ServerRouteCreateOptions" + }, + " | undefined>; }" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 64b4eec298419..84a0b0c457bba 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs- | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 42 | 0 | 42 | 0 | +| 39 | 0 | 39 | 0 | ## Server diff --git a/api_docs/kbn_server_route_repository_client.devdocs.json b/api_docs/kbn_server_route_repository_client.devdocs.json index be6f4e7ecf2d7..03e5c79e96682 100644 --- a/api_docs/kbn_server_route_repository_client.devdocs.json +++ b/api_docs/kbn_server_route_repository_client.devdocs.json @@ -180,7 +180,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => Promise<", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => Promise<", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -225,7 +233,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -251,7 +267,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => ", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -300,7 +324,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -348,7 +380,7 @@ "section": "def-common.ServerRouteCreateOptions", "text": "ServerRouteCreateOptions" }, - "> ? TRouteParamsRT extends ", + " | undefined> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", diff --git a/api_docs/kbn_server_route_repository_client.mdx b/api_docs/kbn_server_route_repository_client.mdx index 9da960e4f48ba..594a8295b991f 100644 --- a/api_docs/kbn_server_route_repository_client.mdx +++ b/api_docs/kbn_server_route_repository_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository-client title: "@kbn/server-route-repository-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository-client plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository-client'] --- import kbnServerRouteRepositoryClientObj from './kbn_server_route_repository_client.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository_utils.devdocs.json b/api_docs/kbn_server_route_repository_utils.devdocs.json index 4f96f6bafd375..b33006a95896a 100644 --- a/api_docs/kbn_server_route_repository_utils.devdocs.json +++ b/api_docs/kbn_server_route_repository_utils.devdocs.json @@ -27,7 +27,15 @@ "label": "formatRequest", "description": [], "signature": [ - "(endpoint: string, pathParams: Record) => { method: Method; pathname: string; version: string; }" + "(endpoint: string, pathParams: Record) => { method: \"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + "; pathname: string; version: string; }" ], "path": "packages/kbn-server-route-repository-utils/src/format_request.ts", "deprecated": false, @@ -75,7 +83,15 @@ "label": "parseEndpoint", "description": [], "signature": [ - "(endpoint: string) => { method: Method; pathname: string; version: string; }" + "(endpoint: string) => { method: \"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + "; pathname: string; version: string; }" ], "path": "packages/kbn-server-route-repository-utils/src/parse_endpoint.ts", "deprecated": false, @@ -102,49 +118,6 @@ } ], "interfaces": [ - { - "parentPluginId": "@kbn/server-route-repository-utils", - "id": "def-common.DefaultRouteCreateOptions", - "type": "Interface", - "tags": [], - "label": "DefaultRouteCreateOptions", - "description": [], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/server-route-repository-utils", - "id": "def-common.DefaultRouteCreateOptions.options", - "type": "Object", - "tags": [], - "label": "options", - "description": [], - "signature": [ - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.RouteConfigOptions", - "text": "RouteConfigOptions" - }, - "<", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.RouteMethod", - "text": "RouteMethod" - }, - "> | undefined" - ], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "@kbn/server-route-repository-utils", "id": "def-common.DefaultRouteHandlerResources", @@ -226,7 +199,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => Promise<", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => Promise<", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -271,7 +252,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -297,7 +286,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>) => ", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -346,7 +343,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions>" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -361,10 +366,10 @@ }, { "parentPluginId": "@kbn/server-route-repository-utils", - "id": "def-common.RouteState", + "id": "def-common.ServerRouteCreateOptions", "type": "Interface", "tags": [], - "label": "RouteState", + "label": "ServerRouteCreateOptions", "description": [], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -372,13 +377,13 @@ "children": [ { "parentPluginId": "@kbn/server-route-repository-utils", - "id": "def-common.RouteState.Unnamed", + "id": "def-common.ServerRouteCreateOptions.Unnamed", "type": "IndexSignature", "tags": [], - "label": "[endpoint: string]: any", + "label": "[x: string]: any", "description": [], "signature": [ - "[endpoint: string]: any" + "[x: string]: any" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -422,7 +427,7 @@ "section": "def-common.ServerRouteCreateOptions", "text": "ServerRouteCreateOptions" }, - "> ? TRouteParamsRT extends ", + " | undefined> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -461,15 +466,7 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " | undefined, any, any, ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.ServerRouteCreateOptions", - "text": "ServerRouteCreateOptions" - }, - "> ? TRouteParamsRT extends ", + " | undefined, any, any, any> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -505,6 +502,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/server-route-repository-utils", + "id": "def-common.DefaultRouteCreateOptions", + "type": "Type", + "tags": [], + "label": "DefaultRouteCreateOptions", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteConfigOptions", + "text": "RouteConfigOptions" + }, + "<\"get\" | ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.DestructiveRouteMethod", + "text": "DestructiveRouteMethod" + }, + ">" + ], + "path": "packages/kbn-server-route-repository-utils/src/typings.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/server-route-repository-utils", "id": "def-common.EndpointOf", @@ -561,15 +588,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - " ? TReturnType extends ", + " ? TReturnType extends ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -631,7 +650,15 @@ "label": "ServerRoute", "description": [], "signature": [ - "{ endpoint: TEndpoint; handler: ServerRouteHandler; } & TRouteCreateOptions & (TRouteParamsRT extends ", + "{ endpoint: TEndpoint; handler: ServerRouteHandler; security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; } & (TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -639,22 +666,15 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " ? { params: TRouteParamsRT; } : {})" - ], - "path": "packages/kbn-server-route-repository-utils/src/typings.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/server-route-repository-utils", - "id": "def-common.ServerRouteCreateOptions", - "type": "Type", - "tags": [], - "label": "ServerRouteCreateOptions", - "description": [], - "signature": [ - "{ [x: string]: any; }" + " ? { params: TRouteParamsRT; } : {}) & (TRouteCreateOptions extends ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.DefaultRouteCreateOptions", + "text": "DefaultRouteCreateOptions" + }, + " ? { options: TRouteCreateOptions; } : {})" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -700,7 +720,15 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " | undefined, any, any, Record>; }" + " | undefined, any, any, ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRouteCreateOptions", + "text": "ServerRouteCreateOptions" + }, + " | undefined>; }" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, diff --git a/api_docs/kbn_server_route_repository_utils.mdx b/api_docs/kbn_server_route_repository_utils.mdx index a317cddd3bd1a..3b615f43bdbc0 100644 --- a/api_docs/kbn_server_route_repository_utils.mdx +++ b/api_docs/kbn_server_route_repository_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository-utils title: "@kbn/server-route-repository-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository-utils'] --- import kbnServerRouteRepositoryUtilsObj from './kbn_server_route_repository_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs- | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 30 | 0 | 30 | 2 | +| 28 | 0 | 28 | 2 | ## Common diff --git a/api_docs/kbn_serverless_common_settings.mdx b/api_docs/kbn_serverless_common_settings.mdx index d24313c36c3ea..5af24bf26b9aa 100644 --- a/api_docs/kbn_serverless_common_settings.mdx +++ b/api_docs/kbn_serverless_common_settings.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-serverless-common-settings title: "@kbn/serverless-common-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-common-settings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-common-settings'] --- import kbnServerlessCommonSettingsObj from './kbn_serverless_common_settings.devdocs.json'; -Contact [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) for questions regarding this plugin. +Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_serverless_observability_settings.mdx b/api_docs/kbn_serverless_observability_settings.mdx index e6d76ca16d65e..a32946e24eb75 100644 --- a/api_docs/kbn_serverless_observability_settings.mdx +++ b/api_docs/kbn_serverless_observability_settings.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-serverless-observability-settings title: "@kbn/serverless-observability-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-observability-settings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-observability-settings'] --- import kbnServerlessObservabilitySettingsObj from './kbn_serverless_observability_settings.devdocs.json'; -Contact [@elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/appex-sharedux ) for questions regarding this plugin. +Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_serverless_project_switcher.mdx b/api_docs/kbn_serverless_project_switcher.mdx index 7ee47b57b9259..9372bb6b82636 100644 --- a/api_docs/kbn_serverless_project_switcher.mdx +++ b/api_docs/kbn_serverless_project_switcher.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-project-switcher title: "@kbn/serverless-project-switcher" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-project-switcher plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-project-switcher'] --- import kbnServerlessProjectSwitcherObj from './kbn_serverless_project_switcher.devdocs.json'; diff --git a/api_docs/kbn_serverless_search_settings.mdx b/api_docs/kbn_serverless_search_settings.mdx index 5e6673894abd2..a2a16b391f4f0 100644 --- a/api_docs/kbn_serverless_search_settings.mdx +++ b/api_docs/kbn_serverless_search_settings.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-serverless-search-settings title: "@kbn/serverless-search-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-search-settings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-search-settings'] --- import kbnServerlessSearchSettingsObj from './kbn_serverless_search_settings.devdocs.json'; -Contact [@elastic/search-kibana @elastic/kibana-management](https://github.com/orgs/elastic/teams/search-kibana ) for questions regarding this plugin. +Contact [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_serverless_security_settings.mdx b/api_docs/kbn_serverless_security_settings.mdx index 955805f53c14f..ffdd8b5556006 100644 --- a/api_docs/kbn_serverless_security_settings.mdx +++ b/api_docs/kbn_serverless_security_settings.mdx @@ -8,14 +8,14 @@ slug: /kibana-dev-docs/api/kbn-serverless-security-settings title: "@kbn/serverless-security-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-security-settings plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-security-settings'] --- import kbnServerlessSecuritySettingsObj from './kbn_serverless_security_settings.devdocs.json'; -Contact [@elastic/security-solution @elastic/kibana-management](https://github.com/orgs/elastic/teams/security-solution ) for questions regarding this plugin. +Contact [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) for questions regarding this plugin. **Code health stats** diff --git a/api_docs/kbn_serverless_storybook_config.mdx b/api_docs/kbn_serverless_storybook_config.mdx index b3e2b555841ac..b01ce815b7f81 100644 --- a/api_docs/kbn_serverless_storybook_config.mdx +++ b/api_docs/kbn_serverless_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-storybook-config title: "@kbn/serverless-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-storybook-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-storybook-config'] --- import kbnServerlessStorybookConfigObj from './kbn_serverless_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index d2d1cbaa29395..ac5533b9ef905 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 21587a72b8258..828275ec41a74 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index 706f666054126..4ed1b3dad4780 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 3e2fca4714ce8..6be9795e43a13 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index fd0d1d4c1baf3..e7a3613ba50af 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 94517cadd7095..9eaf87bfe6f03 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_chrome_navigation.mdx b/api_docs/kbn_shared_ux_chrome_navigation.mdx index 7534b9bfe0a8f..b607d85c38a45 100644 --- a/api_docs/kbn_shared_ux_chrome_navigation.mdx +++ b/api_docs/kbn_shared_ux_chrome_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-chrome-navigation title: "@kbn/shared-ux-chrome-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-chrome-navigation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-chrome-navigation'] --- import kbnSharedUxChromeNavigationObj from './kbn_shared_ux_chrome_navigation.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_error_boundary.mdx b/api_docs/kbn_shared_ux_error_boundary.mdx index e826fee642c04..d02498469d3d4 100644 --- a/api_docs/kbn_shared_ux_error_boundary.mdx +++ b/api_docs/kbn_shared_ux_error_boundary.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-error-boundary title: "@kbn/shared-ux-error-boundary" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-error-boundary plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-error-boundary'] --- import kbnSharedUxErrorBoundaryObj from './kbn_shared_ux_error_boundary.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 26936e7338f72..ce695b9110af2 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index bf5f2668e9e1b..53e7342cc8ac5 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 629569e223d11..fe8fc854a427e 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index b56d10878ac8f..3f94261f74cd4 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index f976ab6f99767..b9a44358a238a 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index 7f3d5139535f8..2b06df69432bb 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index 4c8b98f26dc29..892a61449479b 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 5cbcd0da10669..1eddf7801a4ff 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 957184bb209a5..afdd899fbc816 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 51e107d20ac8f..74f442793491c 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index cc8fd32c04b33..549526ba37a87 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 629b6a7e9fa36..8e9a5640362e4 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index f0e7f5a8ed5b0..cff5ce57bec5c 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index eee4deff4dfa9..264715c6829ee 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index fb41c34b0a7d3..b80ec36ee70ce 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 925dd54b7b219..fd8981a1283cb 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 16bce72a18e5b..5fd147a25611b 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index f4c97a8ec7c5b..d1d156832fd80 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 0a913ddaf832e..ab42168524038 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 3f892eb2a327c..c2ef272b6bbac 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index e80b3adf798fe..cdbaf58b38f87 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index 5be22a01245a3..e016601968f91 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index 7657297435707..2f1c18770d8be 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 9b847c50bb6b1..4994db8600619 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 11302955a772b..306bd2b22ba4a 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index 268aa66431489..0a27157b6556c 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 7676f40f2caf3..424b3544a220c 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index b05834eb29d94..720448f825e3e 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 2ed058123211a..b1d248e211e40 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index 1ba1e92db0a47..347eaed67d639 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_tabbed_modal.mdx b/api_docs/kbn_shared_ux_tabbed_modal.mdx index 81fe8f76a7652..4e0adc9c9f00c 100644 --- a/api_docs/kbn_shared_ux_tabbed_modal.mdx +++ b/api_docs/kbn_shared_ux_tabbed_modal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-tabbed-modal title: "@kbn/shared-ux-tabbed-modal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-tabbed-modal plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-tabbed-modal'] --- import kbnSharedUxTabbedModalObj from './kbn_shared_ux_tabbed_modal.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_table_persist.mdx b/api_docs/kbn_shared_ux_table_persist.mdx index 05e63c3c0a348..a9693db65a6f9 100644 --- a/api_docs/kbn_shared_ux_table_persist.mdx +++ b/api_docs/kbn_shared_ux_table_persist.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-table-persist title: "@kbn/shared-ux-table-persist" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-table-persist plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-table-persist'] --- import kbnSharedUxTablePersistObj from './kbn_shared_ux_table_persist.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 599ae12c50cc9..2be21ab5d350d 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index 82694decc22aa..9bb30ca973831 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index fc741cd9e0549..31fb54dab561d 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_predicates.mdx b/api_docs/kbn_sort_predicates.mdx index e717a22e863e6..8e00e77169968 100644 --- a/api_docs/kbn_sort_predicates.mdx +++ b/api_docs/kbn_sort_predicates.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-predicates title: "@kbn/sort-predicates" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-predicates plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-predicates'] --- import kbnSortPredicatesObj from './kbn_sort_predicates.devdocs.json'; diff --git a/api_docs/kbn_sse_utils.mdx b/api_docs/kbn_sse_utils.mdx index 4782f0f189260..512b2b381ca2c 100644 --- a/api_docs/kbn_sse_utils.mdx +++ b/api_docs/kbn_sse_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils title: "@kbn/sse-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sse-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils'] --- import kbnSseUtilsObj from './kbn_sse_utils.devdocs.json'; diff --git a/api_docs/kbn_sse_utils_client.mdx b/api_docs/kbn_sse_utils_client.mdx index c761b9f95e07c..74d5a7ca98ee3 100644 --- a/api_docs/kbn_sse_utils_client.mdx +++ b/api_docs/kbn_sse_utils_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils-client title: "@kbn/sse-utils-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sse-utils-client plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils-client'] --- import kbnSseUtilsClientObj from './kbn_sse_utils_client.devdocs.json'; diff --git a/api_docs/kbn_sse_utils_server.mdx b/api_docs/kbn_sse_utils_server.mdx index 7e962c561ce61..8acd0ef41ba91 100644 --- a/api_docs/kbn_sse_utils_server.mdx +++ b/api_docs/kbn_sse_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils-server title: "@kbn/sse-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sse-utils-server plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils-server'] --- import kbnSseUtilsServerObj from './kbn_sse_utils_server.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index f79d228215f39..dd30376ddadb1 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 1e1acd7379504..9fac6ebb7c8c9 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index c8fae60176556..d91038307b5c4 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_synthetics_e2e.mdx b/api_docs/kbn_synthetics_e2e.mdx index d56371762c6c9..47eccbb5bfdf0 100644 --- a/api_docs/kbn_synthetics_e2e.mdx +++ b/api_docs/kbn_synthetics_e2e.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-synthetics-e2e title: "@kbn/synthetics-e2e" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/synthetics-e2e plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/synthetics-e2e'] --- import kbnSyntheticsE2eObj from './kbn_synthetics_e2e.devdocs.json'; diff --git a/api_docs/kbn_synthetics_private_location.mdx b/api_docs/kbn_synthetics_private_location.mdx index ac3c669df109c..420efbd1596c0 100644 --- a/api_docs/kbn_synthetics_private_location.mdx +++ b/api_docs/kbn_synthetics_private_location.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-synthetics-private-location title: "@kbn/synthetics-private-location" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/synthetics-private-location plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/synthetics-private-location'] --- import kbnSyntheticsPrivateLocationObj from './kbn_synthetics_private_location.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index d2a8bebfbfa02..ee2ba59bbc03f 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 4540645d39103..0e4ae833e3421 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_eui_helpers.mdx b/api_docs/kbn_test_eui_helpers.mdx index 47d42a7b41456..5064aaa658b03 100644 --- a/api_docs/kbn_test_eui_helpers.mdx +++ b/api_docs/kbn_test_eui_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-eui-helpers title: "@kbn/test-eui-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-eui-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-eui-helpers'] --- import kbnTestEuiHelpersObj from './kbn_test_eui_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 110eb95c420ff..fcb3f8e0fabb6 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index 0bf193f4157ca..9d362121294ad 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_timerange.mdx b/api_docs/kbn_timerange.mdx index e6e3019fe75a5..154ec8e5580ce 100644 --- a/api_docs/kbn_timerange.mdx +++ b/api_docs/kbn_timerange.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-timerange title: "@kbn/timerange" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/timerange plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/timerange'] --- import kbnTimerangeObj from './kbn_timerange.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index cc46129af142a..6a5aa39d549b1 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_transpose_utils.mdx b/api_docs/kbn_transpose_utils.mdx index d29dc282dc1db..f7297c8bc4c78 100644 --- a/api_docs/kbn_transpose_utils.mdx +++ b/api_docs/kbn_transpose_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-transpose-utils title: "@kbn/transpose-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/transpose-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/transpose-utils'] --- import kbnTransposeUtilsObj from './kbn_transpose_utils.devdocs.json'; diff --git a/api_docs/kbn_triggers_actions_ui_types.mdx b/api_docs/kbn_triggers_actions_ui_types.mdx index 93da565cf4d8e..7954618a41f69 100644 --- a/api_docs/kbn_triggers_actions_ui_types.mdx +++ b/api_docs/kbn_triggers_actions_ui_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-triggers-actions-ui-types title: "@kbn/triggers-actions-ui-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/triggers-actions-ui-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/triggers-actions-ui-types'] --- import kbnTriggersActionsUiTypesObj from './kbn_triggers_actions_ui_types.devdocs.json'; diff --git a/api_docs/kbn_try_in_console.mdx b/api_docs/kbn_try_in_console.mdx index 1733473c9cf70..fe6b8736b54d7 100644 --- a/api_docs/kbn_try_in_console.mdx +++ b/api_docs/kbn_try_in_console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-try-in-console title: "@kbn/try-in-console" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/try-in-console plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/try-in-console'] --- import kbnTryInConsoleObj from './kbn_try_in_console.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.devdocs.json b/api_docs/kbn_ts_projects.devdocs.json index 6847c2a76965a..f9a2cddcbfe04 100644 --- a/api_docs/kbn_ts_projects.devdocs.json +++ b/api_docs/kbn_ts_projects.devdocs.json @@ -213,6 +213,22 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/ts-projects", + "id": "def-common.TsProject.isEsm", + "type": "CompoundType", + "tags": [], + "label": "isEsm", + "description": [ + "the package is esm or not" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-ts-projects/ts_project.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/ts-projects", "id": "def-common.TsProject.rootImportReq", diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index 34fc4a73aa965..a2df53a19b78b 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 39 | 0 | 25 | 1 | +| 40 | 0 | 25 | 1 | ## Common diff --git a/api_docs/kbn_typed_react_router_config.devdocs.json b/api_docs/kbn_typed_react_router_config.devdocs.json index 9bab557b023ff..f20c6013c647f 100644 --- a/api_docs/kbn_typed_react_router_config.devdocs.json +++ b/api_docs/kbn_typed_react_router_config.devdocs.json @@ -1,26 +1,10 @@ { "id": "@kbn/typed-react-router-config", "client": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "common": { "classes": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.NotFoundRouteException", + "id": "def-public.NotFoundRouteException", "type": "Class", "tags": [], "label": "NotFoundRouteException", @@ -28,9 +12,9 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.NotFoundRouteException", + "section": "def-public.NotFoundRouteException", "text": "NotFoundRouteException" }, " extends Error" @@ -41,7 +25,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.NotFoundRouteException.Unnamed", + "id": "def-public.NotFoundRouteException.Unnamed", "type": "Function", "tags": [], "label": "Constructor", @@ -55,7 +39,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.NotFoundRouteException.Unnamed.$1", + "id": "def-public.NotFoundRouteException.Unnamed.$1", "type": "string", "tags": [], "label": "message", @@ -78,7 +62,52 @@ "functions": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.createRouter", + "id": "def-public.BreadcrumbsContextProvider", + "type": "Function", + "tags": [], + "label": "BreadcrumbsContextProvider", + "description": [], + "signature": [ + "({\n children,\n}: { children: React.ReactNode; }) => React.JSX.Element" + ], + "path": "packages/kbn-typed-react-router-config/src/breadcrumbs/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.BreadcrumbsContextProvider.$1", + "type": "Object", + "tags": [], + "label": "{\n children,\n}", + "description": [], + "path": "packages/kbn-typed-react-router-config/src/breadcrumbs/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.BreadcrumbsContextProvider.$1.children", + "type": "CompoundType", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "string | number | boolean | React.ReactElement> | Iterable | React.ReactPortal | null | undefined" + ], + "path": "packages/kbn-typed-react-router-config/src/breadcrumbs/context.tsx", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.createRouter", "type": "Function", "tags": [], "label": "createRouter", @@ -87,9 +116,9 @@ "(routes: TRoutes) => ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "" @@ -100,7 +129,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.createRouter.$1", + "id": "def-public.createRouter.$1", "type": "Uncategorized", "tags": [], "label": "routes", @@ -119,7 +148,43 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.CurrentRouteContextProvider", + "id": "def-public.createRouterBreadcrumbComponent", + "type": "Function", + "tags": [], + "label": "createRouterBreadcrumbComponent", + "description": [], + "signature": [ + "() => ", + "RouterBreadcrumb", + "" + ], + "path": "packages/kbn-typed-react-router-config/src/breadcrumbs/create_router_breadcrumb_component.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.createUseBreadcrumbs", + "type": "Function", + "tags": [], + "label": "createUseBreadcrumbs", + "description": [], + "signature": [ + "() => UseBreadcrumbs" + ], + "path": "packages/kbn-typed-react-router-config/src/breadcrumbs/use_router_breadcrumb.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.CurrentRouteContextProvider", "type": "Function", "tags": [], "label": "CurrentRouteContextProvider", @@ -128,17 +193,17 @@ "({ match, element, children, }: { match: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMatch", + "section": "def-public.RouteMatch", "text": "RouteMatch" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" }, ">; element: React.ReactElement>; children: React.ReactElement>; }) => React.JSX.Element" @@ -149,7 +214,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.CurrentRouteContextProvider.$1", + "id": "def-public.CurrentRouteContextProvider.$1", "type": "Object", "tags": [], "label": "{\n match,\n element,\n children,\n}", @@ -160,7 +225,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.CurrentRouteContextProvider.$1.match", + "id": "def-public.CurrentRouteContextProvider.$1.match", "type": "Object", "tags": [], "label": "match", @@ -168,17 +233,17 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMatch", + "section": "def-public.RouteMatch", "text": "RouteMatch" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" }, ">" @@ -189,7 +254,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.CurrentRouteContextProvider.$1.element", + "id": "def-public.CurrentRouteContextProvider.$1.element", "type": "Object", "tags": [], "label": "element", @@ -203,7 +268,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.CurrentRouteContextProvider.$1.children", + "id": "def-public.CurrentRouteContextProvider.$1.children", "type": "Object", "tags": [], "label": "children", @@ -223,7 +288,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Outlet", + "id": "def-public.Outlet", "type": "Function", "tags": [], "label": "Outlet", @@ -240,7 +305,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.OutletContextProvider", + "id": "def-public.OutletContextProvider", "type": "Function", "tags": [], "label": "OutletContextProvider", @@ -254,7 +319,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.OutletContextProvider.$1", + "id": "def-public.OutletContextProvider.$1", "type": "Object", "tags": [], "label": "{\n element,\n children,\n}", @@ -265,7 +330,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.OutletContextProvider.$1.element", + "id": "def-public.OutletContextProvider.$1.element", "type": "Object", "tags": [], "label": "element", @@ -279,7 +344,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.OutletContextProvider.$1.children", + "id": "def-public.OutletContextProvider.$1.children", "type": "CompoundType", "tags": [], "label": "children", @@ -299,7 +364,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterContextProvider", + "id": "def-public.RouterContextProvider", "type": "Function", "tags": [], "label": "RouterContextProvider", @@ -308,17 +373,17 @@ "({ router, children, }: { router: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, ">; children: React.ReactNode; }) => React.JSX.Element" @@ -329,7 +394,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterContextProvider.$1", + "id": "def-public.RouterContextProvider.$1", "type": "Object", "tags": [], "label": "{\n router,\n children,\n}", @@ -340,7 +405,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterContextProvider.$1.router", + "id": "def-public.RouterContextProvider.$1.router", "type": "Object", "tags": [], "label": "router", @@ -348,17 +413,17 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, ">" @@ -369,7 +434,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterContextProvider.$1.children", + "id": "def-public.RouterContextProvider.$1.children", "type": "CompoundType", "tags": [], "label": "children", @@ -389,7 +454,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteRenderer", + "id": "def-public.RouteRenderer", "type": "Function", "tags": [], "label": "RouteRenderer", @@ -406,7 +471,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterProvider", + "id": "def-public.RouterProvider", "type": "Function", "tags": [], "label": "RouterProvider", @@ -415,17 +480,17 @@ "({\n children,\n router,\n history,\n}: { router: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, ">; history: ", @@ -438,7 +503,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterProvider.$1", + "id": "def-public.RouterProvider.$1", "type": "Object", "tags": [], "label": "{\n children,\n router,\n history,\n}", @@ -449,7 +514,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterProvider.$1.router", + "id": "def-public.RouterProvider.$1.router", "type": "Object", "tags": [], "label": "router", @@ -457,17 +522,17 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, ">" @@ -478,7 +543,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterProvider.$1.history", + "id": "def-public.RouterProvider.$1.history", "type": "Object", "tags": [], "label": "history", @@ -493,7 +558,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouterProvider.$1.children", + "id": "def-public.RouterProvider.$1.children", "type": "CompoundType", "tags": [], "label": "children", @@ -513,7 +578,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useCurrentRoute", + "id": "def-public.useCurrentRoute", "type": "Function", "tags": [], "label": "useCurrentRoute", @@ -522,17 +587,17 @@ "() => { match: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMatch", + "section": "def-public.RouteMatch", "text": "RouteMatch" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" }, ">; element: React.ReactElement>; }" @@ -546,7 +611,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useMatchRoutes", + "id": "def-public.useMatchRoutes", "type": "Function", "tags": [], "label": "useMatchRoutes", @@ -555,17 +620,17 @@ "(path: string | undefined) => ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMatch", + "section": "def-public.RouteMatch", "text": "RouteMatch" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" }, ">[]" @@ -576,7 +641,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useMatchRoutes.$1", + "id": "def-public.useMatchRoutes.$1", "type": "string", "tags": [], "label": "path", @@ -595,7 +660,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useParams", + "id": "def-public.useParams", "type": "Function", "tags": [], "label": "useParams", @@ -604,9 +669,9 @@ "(args: any[]) => ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.DefaultOutput", + "section": "def-public.DefaultOutput", "text": "DefaultOutput" }, " | undefined" @@ -617,7 +682,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useParams.$1", + "id": "def-public.useParams.$1", "type": "Array", "tags": [], "label": "args", @@ -636,7 +701,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useRoutePath", + "id": "def-public.useRoutePath", "type": "Function", "tags": [], "label": "useRoutePath", @@ -653,7 +718,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.useRouter", + "id": "def-public.useRouter", "type": "Function", "tags": [], "label": "useRouter", @@ -662,20 +727,12 @@ "() => ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, - "<", - { - "pluginId": "@kbn/typed-react-router-config", - "scope": "common", - "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", - "text": "RouteMap" - }, - ">" + "" ], "path": "packages/kbn-typed-react-router-config/src/use_router.tsx", "deprecated": false, @@ -688,7 +745,7 @@ "interfaces": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.DefaultOutput", + "id": "def-public.DefaultOutput", "type": "Interface", "tags": [], "label": "DefaultOutput", @@ -699,7 +756,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.DefaultOutput.path", + "id": "def-public.DefaultOutput.path", "type": "Object", "tags": [], "label": "path", @@ -713,7 +770,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.DefaultOutput.query", + "id": "def-public.DefaultOutput.query", "type": "Object", "tags": [], "label": "query", @@ -730,7 +787,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route", + "id": "def-public.Route", "type": "Interface", "tags": [], "label": "Route", @@ -741,7 +798,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route.element", + "id": "def-public.Route.element", "type": "Object", "tags": [], "label": "element", @@ -755,7 +812,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route.children", + "id": "def-public.Route.children", "type": "Object", "tags": [], "label": "children", @@ -763,9 +820,9 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, " | undefined" @@ -776,7 +833,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route.params", + "id": "def-public.Route.params", "type": "Object", "tags": [], "label": "params", @@ -791,7 +848,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route.defaults", + "id": "def-public.Route.defaults", "type": "Object", "tags": [], "label": "defaults", @@ -805,7 +862,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Route.pre", + "id": "def-public.Route.pre", "type": "Object", "tags": [], "label": "pre", @@ -822,7 +879,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteMatch", + "id": "def-public.RouteMatch", "type": "Interface", "tags": [], "label": "RouteMatch", @@ -830,9 +887,9 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMatch", + "section": "def-public.RouteMatch", "text": "RouteMatch" }, "" @@ -843,7 +900,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteMatch.route", + "id": "def-public.RouteMatch.route", "type": "CompoundType", "tags": [], "label": "route", @@ -857,7 +914,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteMatch.match", + "id": "def-public.RouteMatch.match", "type": "Object", "tags": [], "label": "match", @@ -878,7 +935,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router", + "id": "def-public.Router", "type": "Interface", "tags": [], "label": "Router", @@ -886,9 +943,9 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Router", + "section": "def-public.Router", "text": "Router" }, "" @@ -899,7 +956,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.matchRoutes", + "id": "def-public.Router.matchRoutes", "type": "Function", "tags": [], "label": "matchRoutes", @@ -908,9 +965,9 @@ "{ >(path: TPath, location: ", @@ -918,9 +975,9 @@ "): ToRouteMatch<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, ">; (location: ", @@ -928,17 +985,17 @@ "): ToRouteMatch<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, ">>; }" @@ -949,7 +1006,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.matchRoutes.$1", + "id": "def-public.Router.matchRoutes.$1", "type": "Uncategorized", "tags": [], "label": "path", @@ -964,7 +1021,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.matchRoutes.$2", + "id": "def-public.Router.matchRoutes.$2", "type": "Object", "tags": [], "label": "location", @@ -983,7 +1040,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.matchRoutes", + "id": "def-public.Router.matchRoutes", "type": "Function", "tags": [], "label": "matchRoutes", @@ -992,9 +1049,9 @@ "{ >(path: TPath, location: ", @@ -1002,9 +1059,9 @@ "): ToRouteMatch<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, ">; (location: ", @@ -1012,17 +1069,17 @@ "): ToRouteMatch<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, ">>; }" @@ -1033,7 +1090,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.matchRoutes.$1", + "id": "def-public.Router.matchRoutes.$1", "type": "Object", "tags": [], "label": "location", @@ -1052,7 +1109,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams", + "id": "def-public.Router.getParams", "type": "Function", "tags": [], "label": "getParams", @@ -1061,9 +1118,9 @@ "{ >(path: TPath, location: ", @@ -1071,17 +1128,17 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1089,33 +1146,33 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, location: ", @@ -1123,41 +1180,41 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ", T3 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, path3: T3, location: ", @@ -1165,33 +1222,33 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1199,17 +1256,17 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; }" @@ -1220,7 +1277,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$1", + "id": "def-public.Router.getParams.$1", "type": "Uncategorized", "tags": [], "label": "path", @@ -1235,7 +1292,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$2", + "id": "def-public.Router.getParams.$2", "type": "Object", "tags": [], "label": "location", @@ -1254,7 +1311,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams", + "id": "def-public.Router.getParams", "type": "Function", "tags": [], "label": "getParams", @@ -1263,9 +1320,9 @@ "{ >(path: TPath, location: ", @@ -1273,17 +1330,17 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1291,33 +1348,33 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, location: ", @@ -1325,41 +1382,41 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ", T3 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, path3: T3, location: ", @@ -1367,33 +1424,33 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1401,17 +1458,17 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; }" @@ -1422,7 +1479,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$1", + "id": "def-public.Router.getParams.$1", "type": "Uncategorized", "tags": [], "label": "path", @@ -1437,7 +1494,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$2", + "id": "def-public.Router.getParams.$2", "type": "Object", "tags": [], "label": "location", @@ -1453,7 +1510,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$3", + "id": "def-public.Router.getParams.$3", "type": "Uncategorized", "tags": [], "label": "optional", @@ -1471,7 +1528,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams", + "id": "def-public.Router.getParams", "type": "Function", "tags": [], "label": "getParams", @@ -1480,9 +1537,9 @@ "{ >(path: TPath, location: ", @@ -1490,17 +1547,17 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1508,33 +1565,33 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, location: ", @@ -1542,41 +1599,41 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ", T3 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, path3: T3, location: ", @@ -1584,33 +1641,33 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1618,17 +1675,17 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; }" @@ -1639,7 +1696,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$1", + "id": "def-public.Router.getParams.$1", "type": "Uncategorized", "tags": [], "label": "path1", @@ -1654,7 +1711,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$2", + "id": "def-public.Router.getParams.$2", "type": "Uncategorized", "tags": [], "label": "path2", @@ -1669,7 +1726,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$3", + "id": "def-public.Router.getParams.$3", "type": "Object", "tags": [], "label": "location", @@ -1688,7 +1745,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams", + "id": "def-public.Router.getParams", "type": "Function", "tags": [], "label": "getParams", @@ -1697,9 +1754,9 @@ "{ >(path: TPath, location: ", @@ -1707,17 +1764,17 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1725,33 +1782,33 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, location: ", @@ -1759,41 +1816,41 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ", T3 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, path3: T3, location: ", @@ -1801,33 +1858,33 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1835,17 +1892,17 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; }" @@ -1856,7 +1913,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$1", + "id": "def-public.Router.getParams.$1", "type": "Uncategorized", "tags": [], "label": "path1", @@ -1871,7 +1928,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$2", + "id": "def-public.Router.getParams.$2", "type": "Uncategorized", "tags": [], "label": "path2", @@ -1886,7 +1943,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$3", + "id": "def-public.Router.getParams.$3", "type": "Uncategorized", "tags": [], "label": "path3", @@ -1901,7 +1958,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$4", + "id": "def-public.Router.getParams.$4", "type": "Object", "tags": [], "label": "location", @@ -1920,7 +1977,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams", + "id": "def-public.Router.getParams", "type": "Function", "tags": [], "label": "getParams", @@ -1929,9 +1986,9 @@ "{ >(path: TPath, location: ", @@ -1939,17 +1996,17 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -1957,33 +2014,33 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, location: ", @@ -1991,41 +2048,41 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , T2 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ", T3 extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, ">(path1: T1, path2: T2, path3: T3, location: ", @@ -2033,33 +2090,33 @@ "): ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; , TOptional extends boolean>(path: TPath, location: ", @@ -2067,17 +2124,17 @@ ", optional: TOptional): TOptional extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, " | undefined : ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, "; }" @@ -2088,7 +2145,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$1", + "id": "def-public.Router.getParams.$1", "type": "Uncategorized", "tags": [], "label": "path", @@ -2103,7 +2160,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$2", + "id": "def-public.Router.getParams.$2", "type": "Object", "tags": [], "label": "location", @@ -2119,7 +2176,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getParams.$3", + "id": "def-public.Router.getParams.$3", "type": "Uncategorized", "tags": [], "label": "optional", @@ -2137,7 +2194,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.link", + "id": "def-public.Router.link", "type": "Function", "tags": [], "label": "link", @@ -2146,25 +2203,25 @@ ">(path: TPath, ...args: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeAsArgs", + "section": "def-public.TypeAsArgs", "text": "TypeAsArgs" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, ">) => string" @@ -2175,7 +2232,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.link.$1", + "id": "def-public.Router.link.$1", "type": "Uncategorized", "tags": [], "label": "path", @@ -2190,7 +2247,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.link.$2", + "id": "def-public.Router.link.$2", "type": "Uncategorized", "tags": [], "label": "args", @@ -2198,17 +2255,17 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeAsArgs", + "section": "def-public.TypeAsArgs", "text": "TypeAsArgs" }, "<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.TypeOf", + "section": "def-public.TypeOf", "text": "TypeOf" }, ">" @@ -2223,7 +2280,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getRoutePath", + "id": "def-public.Router.getRoutePath", "type": "Function", "tags": [], "label": "getRoutePath", @@ -2232,9 +2289,9 @@ "(route: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteWithPath", + "section": "def-public.RouteWithPath", "text": "RouteWithPath" }, ") => string" @@ -2245,7 +2302,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getRoutePath.$1", + "id": "def-public.Router.getRoutePath.$1", "type": "Object", "tags": [], "label": "route", @@ -2253,9 +2310,9 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteWithPath", + "section": "def-public.RouteWithPath", "text": "RouteWithPath" } ], @@ -2269,7 +2326,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getRoutesToMatch", + "id": "def-public.Router.getRoutesToMatch", "type": "Function", "tags": [], "label": "getRoutesToMatch", @@ -2278,9 +2335,9 @@ "(path: string) => ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.FlattenRoutesOf", + "section": "def-public.FlattenRoutesOf", "text": "FlattenRoutesOf" }, "" @@ -2291,7 +2348,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Router.getRoutesToMatch.$1", + "id": "def-public.Router.getRoutesToMatch.$1", "type": "string", "tags": [], "label": "path", @@ -2312,7 +2369,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteWithPath", + "id": "def-public.RouteWithPath", "type": "Interface", "tags": [], "label": "RouteWithPath", @@ -2320,17 +2377,17 @@ "signature": [ { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteWithPath", + "section": "def-public.RouteWithPath", "text": "RouteWithPath" }, " extends ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" } ], @@ -2340,7 +2397,7 @@ "children": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteWithPath.path", + "id": "def-public.RouteWithPath.path", "type": "string", "tags": [], "label": "path", @@ -2357,7 +2414,7 @@ "misc": [ { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.FlattenRoutesOf", + "id": "def-public.FlattenRoutesOf", "type": "Type", "tags": [], "label": "FlattenRoutesOf", @@ -2375,7 +2432,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.Match", + "id": "def-public.Match", "type": "Type", "tags": [], "label": "Match", @@ -2390,7 +2447,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.OutputOf", + "id": "def-public.OutputOf", "type": "Type", "tags": [], "label": "OutputOf", @@ -2399,17 +2456,17 @@ "OutputOfRoutes<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, "> & ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.DefaultOutput", + "section": "def-public.DefaultOutput", "text": "DefaultOutput" } ], @@ -2420,7 +2477,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.PathsOf", + "id": "def-public.PathsOf", "type": "Type", "tags": [], "label": "PathsOf", @@ -2431,9 +2488,9 @@ "<{ [key in keyof TRouteMap]: key | (TRouteMap[key] extends { children: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.RouteMap", + "section": "def-public.RouteMap", "text": "RouteMap" }, "; } ? ", @@ -2441,9 +2498,9 @@ "<`${key & string}/*`> | ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.PathsOf", + "section": "def-public.PathsOf", "text": "PathsOf" }, " : never); }>" @@ -2455,7 +2512,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.RouteMap", + "id": "def-public.RouteMap", "type": "Type", "tags": [], "label": "RouteMap", @@ -2464,9 +2521,9 @@ "{ [x: string]: ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Route", + "section": "def-public.Route", "text": "Route" }, "; }" @@ -2478,7 +2535,7 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.TypeAsArgs", + "id": "def-public.TypeAsArgs", "type": "Type", "tags": [], "label": "TypeAsArgs", @@ -2495,7 +2552,24 @@ }, { "parentPluginId": "@kbn/typed-react-router-config", - "id": "def-common.TypeOf", + "id": "def-public.TypeAsParams", + "type": "Type", + "tags": [], + "label": "TypeAsParams", + "description": [], + "signature": [ + "keyof TObject extends never ? {} : ", + "RequiredKeys", + " extends never ? never : { params: TObject; }" + ], + "path": "packages/kbn-typed-react-router-config/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-public.TypeOf", "type": "Type", "tags": [], "label": "TypeOf", @@ -2504,17 +2578,17 @@ "TypeOfRoutes<", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.Match", + "section": "def-public.Match", "text": "Match" }, "> & (TWithDefaultOutput extends true ? ", { "pluginId": "@kbn/typed-react-router-config", - "scope": "common", + "scope": "public", "docId": "kibKbnTypedReactRouterConfigPluginApi", - "section": "def-common.DefaultOutput", + "section": "def-public.DefaultOutput", "text": "DefaultOutput" }, " : {})" @@ -2526,5 +2600,21 @@ } ], "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] } } \ No newline at end of file diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index f9ca394a7d8d7..6ac21c5e71b9e 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; @@ -21,19 +21,19 @@ Contact [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs- | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 86 | 0 | 86 | 1 | +| 92 | 0 | 92 | 2 | -## Common +## Client ### Functions - + ### Classes - + ### Interfaces - + ### Consts, variables and types - + diff --git a/api_docs/kbn_ui_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 55621afbc3ba4..d55d6709b24dc 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 8183be2c396bc..70408e877b20e 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 7d9158b319934..c2798b1527d7c 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_unified_data_table.mdx b/api_docs/kbn_unified_data_table.mdx index f64d39ea763d5..57154c77cce62 100644 --- a/api_docs/kbn_unified_data_table.mdx +++ b/api_docs/kbn_unified_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-data-table title: "@kbn/unified-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-data-table plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-data-table'] --- import kbnUnifiedDataTableObj from './kbn_unified_data_table.devdocs.json'; diff --git a/api_docs/kbn_unified_doc_viewer.mdx b/api_docs/kbn_unified_doc_viewer.mdx index a0dff14745b50..25cc5d25f6dc4 100644 --- a/api_docs/kbn_unified_doc_viewer.mdx +++ b/api_docs/kbn_unified_doc_viewer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-doc-viewer title: "@kbn/unified-doc-viewer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-doc-viewer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-doc-viewer'] --- import kbnUnifiedDocViewerObj from './kbn_unified_doc_viewer.devdocs.json'; diff --git a/api_docs/kbn_unified_field_list.mdx b/api_docs/kbn_unified_field_list.mdx index 27798da4853c2..388383c3db2dc 100644 --- a/api_docs/kbn_unified_field_list.mdx +++ b/api_docs/kbn_unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-field-list title: "@kbn/unified-field-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-field-list plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-field-list'] --- import kbnUnifiedFieldListObj from './kbn_unified_field_list.devdocs.json'; diff --git a/api_docs/kbn_unsaved_changes_badge.mdx b/api_docs/kbn_unsaved_changes_badge.mdx index bd5fc698832cb..3744ea5c190ea 100644 --- a/api_docs/kbn_unsaved_changes_badge.mdx +++ b/api_docs/kbn_unsaved_changes_badge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unsaved-changes-badge title: "@kbn/unsaved-changes-badge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unsaved-changes-badge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unsaved-changes-badge'] --- import kbnUnsavedChangesBadgeObj from './kbn_unsaved_changes_badge.devdocs.json'; diff --git a/api_docs/kbn_unsaved_changes_prompt.mdx b/api_docs/kbn_unsaved_changes_prompt.mdx index 3d7f1d13ac424..da031414b9e81 100644 --- a/api_docs/kbn_unsaved_changes_prompt.mdx +++ b/api_docs/kbn_unsaved_changes_prompt.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unsaved-changes-prompt title: "@kbn/unsaved-changes-prompt" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unsaved-changes-prompt plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unsaved-changes-prompt'] --- import kbnUnsavedChangesPromptObj from './kbn_unsaved_changes_prompt.devdocs.json'; diff --git a/api_docs/kbn_use_tracked_promise.mdx b/api_docs/kbn_use_tracked_promise.mdx index 595677944190d..bff0ef92d2fd0 100644 --- a/api_docs/kbn_use_tracked_promise.mdx +++ b/api_docs/kbn_use_tracked_promise.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-use-tracked-promise title: "@kbn/use-tracked-promise" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/use-tracked-promise plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/use-tracked-promise'] --- import kbnUseTrackedPromiseObj from './kbn_use_tracked_promise.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index e47bdddd5c2d1..e427cbd8506fa 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.devdocs.json b/api_docs/kbn_utility_types.devdocs.json index 6d177a2aa2cf2..b9cdddfd3ad02 100644 --- a/api_docs/kbn_utility_types.devdocs.json +++ b/api_docs/kbn_utility_types.devdocs.json @@ -361,11 +361,11 @@ "signature": [ "(Exclude<", "ValuesType", - "<{ [TKey in keyof TObject]: {} extends Pick ? ", + "<{ [TKey in keyof TObject as string]: string extends TKey ? Record : {} extends Pick ? ", "DeepPartial", ">> : DedotKey; }>, undefined> extends any ? (k: Exclude<", "ValuesType", - "<{ [TKey in keyof TObject]: {} extends Pick ? ", + "<{ [TKey in keyof TObject as string]: string extends TKey ? Record : {} extends Pick ? ", "DeepPartial", ">> : DedotKey; }>, undefined>) => void : never) extends (k: infer I) => void ? I : never" ], diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 40fe2128fb541..6fffd02afd708 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index a06e735b99af1..68f1c44d0929e 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index a25ca1af3e9a6..e6ae404396de6 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_visualization_ui_components.mdx b/api_docs/kbn_visualization_ui_components.mdx index 0a0219896da7d..c6064a6fbe6da 100644 --- a/api_docs/kbn_visualization_ui_components.mdx +++ b/api_docs/kbn_visualization_ui_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-ui-components title: "@kbn/visualization-ui-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/visualization-ui-components plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-ui-components'] --- import kbnVisualizationUiComponentsObj from './kbn_visualization_ui_components.devdocs.json'; diff --git a/api_docs/kbn_visualization_utils.mdx b/api_docs/kbn_visualization_utils.mdx index 940e0a88cd7ec..cbfbdd595cf41 100644 --- a/api_docs/kbn_visualization_utils.mdx +++ b/api_docs/kbn_visualization_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-utils title: "@kbn/visualization-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/visualization-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-utils'] --- import kbnVisualizationUtilsObj from './kbn_visualization_utils.devdocs.json'; diff --git a/api_docs/kbn_xstate_utils.mdx b/api_docs/kbn_xstate_utils.mdx index df3dff9fb6c90..94c73215d817b 100644 --- a/api_docs/kbn_xstate_utils.mdx +++ b/api_docs/kbn_xstate_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-xstate-utils title: "@kbn/xstate-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/xstate-utils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/xstate-utils'] --- import kbnXstateUtilsObj from './kbn_xstate_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 2837099abf79d..72af88d57102a 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kbn_zod.devdocs.json b/api_docs/kbn_zod.devdocs.json index 4fbf59e5edf59..9f1ada94d8e67 100644 --- a/api_docs/kbn_zod.devdocs.json +++ b/api_docs/kbn_zod.devdocs.json @@ -19198,7 +19198,7 @@ "label": "ZodFirstPartySchemaTypes", "description": [], "signature": [ - "Zod.ZodString | Zod.ZodBoolean | Zod.ZodNumber | Zod.ZodUnknown | Zod.ZodNull | Zod.ZodAny | Zod.ZodUndefined | Zod.ZodBigInt | Zod.ZodDate | Zod.ZodSymbol | Zod.ZodNever | Zod.ZodVoid | Zod.ZodTuple | Zod.ZodNaN | Zod.ZodArray | Zod.ZodObject | Zod.ZodUnion | Zod.ZodDiscriminatedUnion | Zod.ZodIntersection | Zod.ZodRecord | Zod.ZodMap | Zod.ZodSet | Zod.ZodFunction | Zod.ZodLazy | Zod.ZodLiteral | Zod.ZodEnum | Zod.ZodEffects | Zod.ZodNativeEnum | Zod.ZodOptional | Zod.ZodNullable | Zod.ZodDefault | Zod.ZodCatch | Zod.ZodPromise | Zod.ZodBranded | Zod.ZodPipeline | Zod.ZodReadonly" + "Zod.ZodString | Zod.ZodBoolean | Zod.ZodNumber | Zod.ZodNull | Zod.ZodUnknown | Zod.ZodAny | Zod.ZodUndefined | Zod.ZodBigInt | Zod.ZodDate | Zod.ZodSymbol | Zod.ZodNever | Zod.ZodVoid | Zod.ZodTuple | Zod.ZodNaN | Zod.ZodArray | Zod.ZodObject | Zod.ZodUnion | Zod.ZodDiscriminatedUnion | Zod.ZodIntersection | Zod.ZodRecord | Zod.ZodMap | Zod.ZodSet | Zod.ZodFunction | Zod.ZodLazy | Zod.ZodLiteral | Zod.ZodEnum | Zod.ZodEffects | Zod.ZodNativeEnum | Zod.ZodOptional | Zod.ZodNullable | Zod.ZodDefault | Zod.ZodCatch | Zod.ZodPromise | Zod.ZodBranded | Zod.ZodPipeline | Zod.ZodReadonly" ], "path": "node_modules/zod/lib/types.d.ts", "deprecated": false, diff --git a/api_docs/kbn_zod.mdx b/api_docs/kbn_zod.mdx index c41764cb16c3c..3b2c6c9426c52 100644 --- a/api_docs/kbn_zod.mdx +++ b/api_docs/kbn_zod.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-zod title: "@kbn/zod" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/zod plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/zod'] --- import kbnZodObj from './kbn_zod.devdocs.json'; diff --git a/api_docs/kbn_zod_helpers.mdx b/api_docs/kbn_zod_helpers.mdx index 80f634d7b5556..e05b28083dde0 100644 --- a/api_docs/kbn_zod_helpers.mdx +++ b/api_docs/kbn_zod_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-zod-helpers title: "@kbn/zod-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/zod-helpers plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/zod-helpers'] --- import kbnZodHelpersObj from './kbn_zod_helpers.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 988b049574fc7..3e9be85719671 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 133ca02a6cbc1..a6f0de081762f 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 4c68a44bbbb3f..a1589a4c09545 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index b7e342898acbb..7d0e464a720b7 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index f383c13e0c1c9..0c9d7a8273d47 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 469c1f399b3ad..bd9615bffba59 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 5879ddb4b9818..d899125d85fd8 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index da1f9abca6102..18faeeaf9fe97 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/links.mdx b/api_docs/links.mdx index 9a09412f4ec36..2f2744df39473 100644 --- a/api_docs/links.mdx +++ b/api_docs/links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/links title: "links" image: https://source.unsplash.com/400x175/?github description: API docs for the links plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'links'] --- import linksObj from './links.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 780e650de1669..4359102b2f850 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/llm_tasks.mdx b/api_docs/llm_tasks.mdx index 29e2038811265..64a0480743e16 100644 --- a/api_docs/llm_tasks.mdx +++ b/api_docs/llm_tasks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/llmTasks title: "llmTasks" image: https://source.unsplash.com/400x175/?github description: API docs for the llmTasks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'llmTasks'] --- import llmTasksObj from './llm_tasks.devdocs.json'; diff --git a/api_docs/logs_data_access.mdx b/api_docs/logs_data_access.mdx index 329560fefeeb9..f519b78e8d3d7 100644 --- a/api_docs/logs_data_access.mdx +++ b/api_docs/logs_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsDataAccess title: "logsDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the logsDataAccess plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsDataAccess'] --- import logsDataAccessObj from './logs_data_access.devdocs.json'; diff --git a/api_docs/logs_explorer.mdx b/api_docs/logs_explorer.mdx index 72054cd0a16a0..2c01072a671dd 100644 --- a/api_docs/logs_explorer.mdx +++ b/api_docs/logs_explorer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsExplorer title: "logsExplorer" image: https://source.unsplash.com/400x175/?github description: API docs for the logsExplorer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsExplorer'] --- import logsExplorerObj from './logs_explorer.devdocs.json'; diff --git a/api_docs/logs_shared.mdx b/api_docs/logs_shared.mdx index 34a08a7a8a62b..3caa322fa2b8e 100644 --- a/api_docs/logs_shared.mdx +++ b/api_docs/logs_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsShared title: "logsShared" image: https://source.unsplash.com/400x175/?github description: API docs for the logsShared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsShared'] --- import logsSharedObj from './logs_shared.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 17ab011cd0c20..73aa58c63c4b5 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index f58e7a92e557e..706136b3ba414 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index d715644222a06..04e10bd9621b4 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/metrics_data_access.mdx b/api_docs/metrics_data_access.mdx index efcc57e6a8439..77743d2ab64eb 100644 --- a/api_docs/metrics_data_access.mdx +++ b/api_docs/metrics_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/metricsDataAccess title: "metricsDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the metricsDataAccess plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'metricsDataAccess'] --- import metricsDataAccessObj from './metrics_data_access.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 758784316b32e..2a49a37a5e257 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/mock_idp_plugin.mdx b/api_docs/mock_idp_plugin.mdx index 5b30e3fd06ffa..ad9da5bc80c4a 100644 --- a/api_docs/mock_idp_plugin.mdx +++ b/api_docs/mock_idp_plugin.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mockIdpPlugin title: "mockIdpPlugin" image: https://source.unsplash.com/400x175/?github description: API docs for the mockIdpPlugin plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mockIdpPlugin'] --- import mockIdpPluginObj from './mock_idp_plugin.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 56b0c4de6567d..66318096eb922 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 244e38b15e56a..4fd289aa19adb 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index 1f41705561075..fe5b59379a4db 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index f2dba3b503802..1905d9ecf9080 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/no_data_page.mdx b/api_docs/no_data_page.mdx index b6a06908fff43..0a4821a723677 100644 --- a/api_docs/no_data_page.mdx +++ b/api_docs/no_data_page.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/noDataPage title: "noDataPage" image: https://source.unsplash.com/400x175/?github description: API docs for the noDataPage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'noDataPage'] --- import noDataPageObj from './no_data_page.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 5b78cd03a1349..40517aef8a056 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 7ff6ef019cc98..48c5cad2a518a 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -2863,6 +2863,27 @@ "path": "x-pack/plugins/observability_solution/observability/public/plugin.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsSetup.streams", + "type": "Object", + "tags": [], + "label": "streams", + "description": [], + "signature": [ + { + "pluginId": "streams", + "scope": "public", + "docId": "kibStreamsPluginApi", + "section": "def-public.StreamsPluginSetup", + "text": "StreamsPluginSetup" + }, + " | undefined" + ], + "path": "x-pack/plugins/observability_solution/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3813,6 +3834,27 @@ "path": "x-pack/plugins/observability_solution/observability/public/plugin.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsStart.streams", + "type": "Object", + "tags": [], + "label": "streams", + "description": [], + "signature": [ + { + "pluginId": "streams", + "scope": "public", + "docId": "kibStreamsPluginApi", + "section": "def-public.StreamsPluginStart", + "text": "StreamsPluginStart" + }, + " | undefined" + ], + "path": "x-pack/plugins/observability_solution/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -8592,13 +8634,27 @@ "children": [ { "parentPluginId": "observability", - "id": "def-server.ObservabilityRouteCreateOptions.options", - "type": "Object", + "id": "def-server.ObservabilityRouteCreateOptions.tags", + "type": "Array", "tags": [], - "label": "options", + "label": "tags", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/observability_solution/observability/server/routes/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "observability", + "id": "def-server.ObservabilityRouteCreateOptions.access", + "type": "CompoundType", + "tags": [], + "label": "access", "description": [], "signature": [ - "{ tags: string[]; access?: \"internal\" | \"public\" | undefined; }" + "\"internal\" | \"public\" | undefined" ], "path": "x-pack/plugins/observability_solution/observability/server/routes/types.ts", "deprecated": false, @@ -8717,20 +8773,6 @@ "path": "x-pack/plugins/observability_solution/observability/server/routes/types.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "observability", - "id": "def-server.ObservabilityRouteHandlerResources.config", - "type": "Object", - "tags": [], - "label": "config", - "description": [], - "signature": [ - "{ readonly enabled: boolean; readonly annotations: Readonly<{} & { index: string; enabled: boolean; }>; readonly unsafe: Readonly<{} & { alertDetails: Readonly<{} & { uptime: Readonly<{} & { enabled: boolean; }>; observability: Readonly<{} & { enabled: boolean; }>; metrics: Readonly<{} & { enabled: boolean; }>; logs: Readonly<{} & { enabled: boolean; }>; }>; thresholdRule: Readonly<{} & { enabled: boolean; }>; ruleFormV2: Readonly<{} & { enabled: boolean; }>; }>; readonly customThresholdRule: Readonly<{} & { groupByPageSize: number; }>; readonly createO11yGenericFeatureId: boolean; }" - ], - "path": "x-pack/plugins/observability_solution/observability/server/routes/types.ts", - "deprecated": false, - "trackAdoption": false } ], "initialIsOpen": false @@ -8762,7 +8804,15 @@ "section": "def-common.RouteParamsRT", "text": "RouteParamsRT" }, - " | undefined, any, any, Record>; }" + " | undefined, any, any, ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRouteCreateOptions", + "text": "ServerRouteCreateOptions" + }, + " | undefined>; }" ], "path": "x-pack/plugins/observability_solution/observability/server/routes/types.ts", "deprecated": false, @@ -8922,15 +8972,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - " ? TReturnType extends ", + " ? TReturnType extends ", { "pluginId": "@kbn/core-http-server", "scope": "server", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index e13a0cdeffff6..db6a0f97c7f77 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/ | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 695 | 2 | 687 | 23 | +| 697 | 2 | 689 | 23 | ## Client diff --git a/api_docs/observability_a_i_assistant.devdocs.json b/api_docs/observability_a_i_assistant.devdocs.json index 913dfebf5bf4f..a11aa4c4548bd 100644 --- a/api_docs/observability_a_i_assistant.devdocs.json +++ b/api_docs/observability_a_i_assistant.devdocs.json @@ -3159,7 +3159,15 @@ "Readable", ", ", "ObservabilityAIAssistantRouteCreateOptions", - ">; }, TEndpoint> & Omit & { signal: AbortSignal | null; }>) => Promise<", + ">; }, TEndpoint> & Omit & { signal: AbortSignal | null; } & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + ">) => Promise<", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -4169,7 +4177,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions> extends never ? [] | [", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "> extends never ? [] | [", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -4177,7 +4193,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions] : [", + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "] : [", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -4185,7 +4209,15 @@ "section": "def-common.ClientRequestParamsOf", "text": "ClientRequestParamsOf" }, - " & TAdditionalClientOptions]" + " & TAdditionalClientOptions & ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "public", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-public.HttpFetchOptions", + "text": "HttpFetchOptions" + }, + "]" ], "path": "packages/kbn-server-route-repository-utils/src/typings.ts", "deprecated": false, @@ -5832,15 +5864,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - " ? TReturnType extends ", + " ? TReturnType extends ", { "pluginId": "@kbn/core-http-server", "scope": "server", @@ -7063,7 +7087,7 @@ "section": "def-common.ServerRouteCreateOptions", "text": "ServerRouteCreateOptions" }, - "> ? TRouteParamsRT extends ", + " | undefined> ? TRouteParamsRT extends ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", diff --git a/api_docs/observability_a_i_assistant.mdx b/api_docs/observability_a_i_assistant.mdx index c583cc169e75a..83d6326878766 100644 --- a/api_docs/observability_a_i_assistant.mdx +++ b/api_docs/observability_a_i_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistant title: "observabilityAIAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityAIAssistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistant'] --- import observabilityAIAssistantObj from './observability_a_i_assistant.devdocs.json'; diff --git a/api_docs/observability_a_i_assistant_app.mdx b/api_docs/observability_a_i_assistant_app.mdx index 11da1cf5a2c56..4b6a287761479 100644 --- a/api_docs/observability_a_i_assistant_app.mdx +++ b/api_docs/observability_a_i_assistant_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistantApp title: "observabilityAIAssistantApp" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityAIAssistantApp plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistantApp'] --- import observabilityAIAssistantAppObj from './observability_a_i_assistant_app.devdocs.json'; diff --git a/api_docs/observability_ai_assistant_management.mdx b/api_docs/observability_ai_assistant_management.mdx index 8ab2193dec672..6ec6533a095c7 100644 --- a/api_docs/observability_ai_assistant_management.mdx +++ b/api_docs/observability_ai_assistant_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAiAssistantManagement title: "observabilityAiAssistantManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityAiAssistantManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAiAssistantManagement'] --- import observabilityAiAssistantManagementObj from './observability_ai_assistant_management.devdocs.json'; diff --git a/api_docs/observability_logs_explorer.mdx b/api_docs/observability_logs_explorer.mdx index 01d2931bc5a82..bdac03ae09a8c 100644 --- a/api_docs/observability_logs_explorer.mdx +++ b/api_docs/observability_logs_explorer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityLogsExplorer title: "observabilityLogsExplorer" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityLogsExplorer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityLogsExplorer'] --- import observabilityLogsExplorerObj from './observability_logs_explorer.devdocs.json'; diff --git a/api_docs/observability_onboarding.mdx b/api_docs/observability_onboarding.mdx index 9f205d47a6380..f7778665f7c2b 100644 --- a/api_docs/observability_onboarding.mdx +++ b/api_docs/observability_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityOnboarding title: "observabilityOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityOnboarding plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityOnboarding'] --- import observabilityOnboardingObj from './observability_onboarding.devdocs.json'; diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 878e058030c2c..dfd831976712e 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 64e5de0e2b420..ef91a75573680 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/painless_lab.mdx b/api_docs/painless_lab.mdx index 3b7ec39b0ea81..662fa13379175 100644 --- a/api_docs/painless_lab.mdx +++ b/api_docs/painless_lab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/painlessLab title: "painlessLab" image: https://source.unsplash.com/400x175/?github description: API docs for the painlessLab plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'painlessLab'] --- import painlessLabObj from './painless_lab.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index bacd3236a5a31..70a50eac81a70 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 887 | 757 | 47 | +| 892 | 760 | 43 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 54464 | 247 | 40907 | 2015 | +| 54552 | 240 | 40981 | 2020 | ## Plugin Directory @@ -57,7 +57,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 54 | 0 | 51 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3209 | 31 | 2594 | 24 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 6 | 0 | 6 | 0 | -| | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 10 | 0 | 10 | 0 | +| | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 9 | 0 | 9 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 35 | 0 | 25 | 5 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Reusable data view field editor across Kibana | 72 | 0 | 33 | 1 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data view management app | 2 | 0 | 2 | 0 | @@ -75,7 +75,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 54 | 0 | 47 | 1 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | Adds dashboards for discovering and managing Enterprise Search products. | 5 | 0 | 5 | 0 | | | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | - | 2 | 0 | 2 | 0 | -| | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | Entity manager plugin for entity assets (inventory, topology, etc) | 20 | 0 | 20 | 3 | +| | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | Entity manager plugin for entity assets (inventory, topology, etc) | 35 | 0 | 35 | 2 | +| entityManagerApp | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | Entity manager plugin for entity assets (inventory, topology, etc) | 0 | 0 | 0 | 0 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 99 | 3 | 97 | 3 | | | [@elastic/kibana-esql](https://github.com/orgs/elastic/teams/kibana-esql) | - | 25 | 0 | 9 | 0 | | | [@elastic/kibana-esql](https://github.com/orgs/elastic/teams/kibana-esql) | - | 2 | 0 | 2 | 0 | @@ -103,7 +104,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 89 | 0 | 89 | 8 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 240 | 0 | 24 | 9 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Simple UI for managing files in Kibana | 3 | 0 | 3 | 0 | -| | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1428 | 5 | 1303 | 81 | +| | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1430 | 5 | 1304 | 82 | | ftrApis | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 72 | 0 | 14 | 5 | | globalSearchBar | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 0 | 0 | 0 | 0 | @@ -153,7 +154,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 1 | -| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 695 | 2 | 687 | 23 | +| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 697 | 2 | 689 | 23 | | | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 296 | 1 | 294 | 27 | | | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 4 | 0 | 4 | 0 | | | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 2 | 0 | 2 | 0 | @@ -185,6 +186,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 18 | 0 | 10 | 0 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 18 | 0 | 18 | 0 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 11 | 0 | 7 | 1 | +| | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 21 | 0 | 21 | 0 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | Plugin to provide access to and rendering of python notebooks for use in the persistent developer console. | 10 | 0 | 10 | 1 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 22 | 0 | 16 | 1 | | searchprofiler | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 0 | 0 | 0 | 0 | @@ -202,7 +204,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 269 | 0 | 73 | 1 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 25 | 0 | 25 | 3 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 10 | 0 | 10 | 0 | -| | @simianhacker @flash1293 @dgieselaar | A manager for Streams | 12 | 7 | 12 | 2 | +| | @simianhacker @flash1293 @dgieselaar | A manager for Streams | 13 | 0 | 13 | 4 | +| | @simianhacker @flash1293 @dgieselaar | - | 8 | 0 | 8 | 0 | | synthetics | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions. | 0 | 0 | 0 | 1 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 108 | 0 | 64 | 7 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 45 | 0 | 1 | 0 | @@ -210,7 +213,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | telemetryCollectionXpack | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 0 | 0 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 30 | 0 | 14 | 4 | -| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 226 | 1 | 182 | 17 | +| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 236 | 1 | 192 | 18 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [@elastic/kibana-localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 594 | 1 | 568 | 51 | @@ -263,7 +266,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 18 | 0 | 18 | 0 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 4 | 0 | 4 | 0 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 87 | 0 | 87 | 11 | -| | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 268 | 0 | 268 | 38 | +| | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 270 | 0 | 270 | 38 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 337 | 0 | 336 | 0 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 12 | 0 | 12 | 0 | | | [@elastic/security-defend-workflows](https://github.com/orgs/elastic/teams/security-defend-workflows) | - | 3 | 0 | 3 | 0 | @@ -377,9 +380,10 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 54 | 7 | 54 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 15 | 0 | 15 | 1 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 568 | 2 | 242 | 0 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 567 | 2 | 242 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 96 | 0 | 83 | 10 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 46 | 0 | 45 | 0 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 2 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 2 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 3 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 1 | 0 | @@ -509,7 +513,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 38 | 2 | 33 | 0 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 37 | 0 | 34 | 2 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 284 | 0 | 234 | 4 | -| | [@elastic/docs](https://github.com/orgs/elastic/teams/docs) | - | 78 | 0 | 78 | 2 | +| | [@elastic/docs](https://github.com/orgs/elastic/teams/docs) | - | 79 | 0 | 79 | 2 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 5 | 0 | 5 | 1 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 57 | 0 | 30 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 37 | 0 | 28 | 2 | @@ -517,7 +521,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 42 | 0 | 41 | 0 | | | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | - | 169 | 0 | 140 | 10 | | | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | - | 442 | 0 | 405 | 0 | -| | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | - | 45 | 0 | 45 | 0 | +| | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | - | 50 | 0 | 50 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 55 | 0 | 40 | 7 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 32 | 0 | 19 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 11 | 0 | 6 | 0 | @@ -581,8 +585,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 23 | 0 | 7 | 0 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 8 | 0 | 2 | 3 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 45 | 0 | 0 | 0 | -| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 140 | 0 | 139 | 0 | -| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 20 | 0 | 11 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 140 | 0 | 139 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 20 | 0 | 11 | 0 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 88 | 0 | 10 | 0 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 56 | 0 | 6 | 0 | | | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 2 | 0 | 0 | 0 | @@ -639,7 +643,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 1 | 0 | 1 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 1 | 0 | 1 | 0 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 92 | 0 | 80 | 0 | -| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 224 | 0 | 188 | 6 | +| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 228 | 0 | 192 | 6 | | | [@elastic/appex-ai-infra](https://github.com/orgs/elastic/teams/appex-ai-infra) | - | 1 | 0 | 1 | 0 | | | [@elastic/appex-ai-infra](https://github.com/orgs/elastic/teams/appex-ai-infra) | - | 31 | 0 | 31 | 0 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 168 | 0 | 55 | 0 | @@ -656,7 +660,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 18 | 0 | 18 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 40 | 0 | 38 | 5 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 13 | 0 | 9 | 0 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 6 | 0 | 6 | 1 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 14 | 0 | 7 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 98 | 0 | 88 | 13 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 13 | 0 | 13 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 36 | 0 | 36 | 2 | @@ -719,16 +723,16 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 211 | 0 | 162 | 0 | | | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 28 | 0 | 25 | 0 | | | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 120 | 0 | 116 | 0 | -| | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 60 | 0 | 54 | 0 | +| | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 63 | 0 | 55 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 69 | 0 | 64 | 0 | -| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 42 | 0 | 42 | 0 | +| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 39 | 0 | 39 | 0 | | | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 15 | 0 | 15 | 0 | -| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 30 | 0 | 30 | 2 | -| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 1 | 0 | 1 | 0 | -| | [@elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 1 | 0 | 1 | 0 | +| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 28 | 0 | 28 | 2 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 1 | 0 | 1 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 1 | 0 | 1 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 7 | 1 | -| | [@elastic/search-kibana @elastic/kibana-management](https://github.com/orgs/elastic/teams/search-kibana ) | - | 1 | 0 | 1 | 0 | -| | [@elastic/security-solution @elastic/kibana-management](https://github.com/orgs/elastic/teams/security-solution ) | - | 1 | 0 | 1 | 0 | +| | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 1 | 0 | 1 | 0 | +| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 1 | 0 | 1 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 0 | 0 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 5 | 0 | 5 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 2 | 2 | @@ -792,8 +796,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 9 | 0 | 7 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 15 | 0 | 15 | 0 | | | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 2 | 0 | 2 | 1 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 39 | 0 | 25 | 1 | -| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 86 | 0 | 86 | 1 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 40 | 0 | 25 | 1 | +| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 92 | 0 | 92 | 2 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 42 | 0 | 28 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 61 | 0 | 52 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 9 | 0 | 8 | 0 | diff --git a/api_docs/presentation_panel.mdx b/api_docs/presentation_panel.mdx index 7b01ef460a9ca..dcf4c63e6fa94 100644 --- a/api_docs/presentation_panel.mdx +++ b/api_docs/presentation_panel.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationPanel title: "presentationPanel" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationPanel plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationPanel'] --- import presentationPanelObj from './presentation_panel.devdocs.json'; diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 370b3a29ab49c..eb5ea5d344720 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/product_doc_base.mdx b/api_docs/product_doc_base.mdx index 6f37accb23278..64daeee07e961 100644 --- a/api_docs/product_doc_base.mdx +++ b/api_docs/product_doc_base.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/productDocBase title: "productDocBase" image: https://source.unsplash.com/400x175/?github description: API docs for the productDocBase plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'productDocBase'] --- import productDocBaseObj from './product_doc_base.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index 84148793290ba..d4e574ee7c537 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/profiling_data_access.mdx b/api_docs/profiling_data_access.mdx index a126eee9f1305..c2e42aea3719c 100644 --- a/api_docs/profiling_data_access.mdx +++ b/api_docs/profiling_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profilingDataAccess title: "profilingDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the profilingDataAccess plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profilingDataAccess'] --- import profilingDataAccessObj from './profiling_data_access.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index ffb7dafceff4f..06bb00664875b 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 77ab774652a23..b527b3781de40 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 5c6de0ea3fea0..cb2de04b35033 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 0d285a2b751dd..cb5f1c312fee6 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 05ec2fb29f0ff..8035d2d528582 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index d3a810154ffa1..a613f2e9e51ca 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index 7ef035785acd4..f1d7ffd140e81 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 607032b173e1a..297b97bb5d3fb 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 38664c94a97b4..7a76335945263 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 6b9877691d615..5e10e78038d1f 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index a57d40cdae1fb..3e8c5afe0f513 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 0409c2b38ef80..dd3181adece13 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index ab0e270d9f0cd..1b475d9f8e81f 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/search_assistant.mdx b/api_docs/search_assistant.mdx index f67cffe88f106..b08628810c98c 100644 --- a/api_docs/search_assistant.mdx +++ b/api_docs/search_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchAssistant title: "searchAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the searchAssistant plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchAssistant'] --- import searchAssistantObj from './search_assistant.devdocs.json'; diff --git a/api_docs/search_connectors.mdx b/api_docs/search_connectors.mdx index b46fb0f713d3f..ec297198df273 100644 --- a/api_docs/search_connectors.mdx +++ b/api_docs/search_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchConnectors title: "searchConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the searchConnectors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchConnectors'] --- import searchConnectorsObj from './search_connectors.devdocs.json'; diff --git a/api_docs/search_homepage.mdx b/api_docs/search_homepage.mdx index 80b0b327debff..608226b0265db 100644 --- a/api_docs/search_homepage.mdx +++ b/api_docs/search_homepage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchHomepage title: "searchHomepage" image: https://source.unsplash.com/400x175/?github description: API docs for the searchHomepage plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchHomepage'] --- import searchHomepageObj from './search_homepage.devdocs.json'; diff --git a/api_docs/search_indices.devdocs.json b/api_docs/search_indices.devdocs.json index 8b486873528f8..8446bc09cbb0d 100644 --- a/api_docs/search_indices.devdocs.json +++ b/api_docs/search_indices.devdocs.json @@ -85,7 +85,7 @@ "label": "startAppId", "description": [], "signature": [ - "\"fleet\" | \"graph\" | \"ml\" | \"monitoring\" | \"profiling\" | \"metrics\" | \"management\" | \"apm\" | \"synthetics\" | \"ux\" | \"canvas\" | \"logs\" | \"dashboards\" | \"slo\" | \"observabilityAIAssistant\" | \"home\" | \"integrations\" | \"discover\" | \"observability-overview\" | \"appSearch\" | \"dev_tools\" | \"maps\" | \"visualize\" | \"dev_tools:console\" | \"dev_tools:searchprofiler\" | \"dev_tools:painless_lab\" | \"dev_tools:grokdebugger\" | \"ml:notifications\" | \"ml:nodes\" | \"ml:overview\" | \"ml:memoryUsage\" | \"ml:settings\" | \"ml:dataVisualizer\" | \"ml:logPatternAnalysis\" | \"ml:logRateAnalysis\" | \"ml:singleMetricViewer\" | \"ml:anomalyDetection\" | \"ml:anomalyExplorer\" | \"ml:dataDrift\" | \"ml:dataFrameAnalytics\" | \"ml:resultExplorer\" | \"ml:analyticsMap\" | \"ml:aiOps\" | \"ml:changePointDetections\" | \"ml:modelManagement\" | \"ml:nodesOverview\" | \"ml:esqlDataVisualizer\" | \"ml:fileUpload\" | \"ml:indexDataVisualizer\" | \"ml:calendarSettings\" | \"ml:filterListsSettings\" | \"ml:suppliedConfigurations\" | \"osquery\" | \"management:transform\" | \"management:watcher\" | \"management:cases\" | \"management:tags\" | \"management:maintenanceWindows\" | \"management:cross_cluster_replication\" | \"management:dataViews\" | \"management:spaces\" | \"management:settings\" | \"management:users\" | \"management:migrate_data\" | \"management:search_sessions\" | \"management:data_quality\" | \"management:filesManagement\" | \"management:roles\" | \"management:reporting\" | \"management:aiAssistantManagementSelection\" | \"management:securityAiAssistantManagement\" | \"management:observabilityAiAssistantManagement\" | \"management:api_keys\" | \"management:license_management\" | \"management:index_lifecycle_management\" | \"management:index_management\" | \"management:ingest_pipelines\" | \"management:jobsListLink\" | \"management:objects\" | \"management:pipelines\" | \"management:remote_clusters\" | \"management:role_mappings\" | \"management:rollup_jobs\" | \"management:snapshot_restore\" | \"management:triggersActions\" | \"management:triggersActionsConnectors\" | \"management:upgrade_assistant\" | \"enterpriseSearch\" | \"enterpriseSearchContent\" | \"enterpriseSearchApplications\" | \"searchInferenceEndpoints\" | \"enterpriseSearchAnalytics\" | \"workplaceSearch\" | \"serverlessElasticsearch\" | \"serverlessConnectors\" | \"serverlessWebCrawlers\" | \"searchPlayground\" | \"searchHomepage\" | \"enterpriseSearchContent:connectors\" | \"enterpriseSearchContent:searchIndices\" | \"enterpriseSearchContent:webCrawlers\" | \"enterpriseSearchApplications:searchApplications\" | \"enterpriseSearchApplications:playground\" | \"appSearch:engines\" | \"searchInferenceEndpoints:inferenceEndpoints\" | \"elasticsearchStart\" | \"elasticsearchIndices\" | \"enterpriseSearchElasticsearch\" | \"enterpriseSearchVectorSearch\" | \"enterpriseSearchSemanticSearch\" | \"enterpriseSearchAISearch\" | \"elasticsearchIndices:createIndex\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\" | \"fleet:settings\" | \"fleet:agents\" | \"fleet:policies\" | \"fleet:data_streams\" | \"fleet:enrollment_tokens\" | \"fleet:uninstall_tokens\"" + "\"fleet\" | \"graph\" | \"ml\" | \"monitoring\" | \"profiling\" | \"metrics\" | \"management\" | \"apm\" | \"synthetics\" | \"ux\" | \"canvas\" | \"logs\" | \"dashboards\" | \"slo\" | \"observabilityAIAssistant\" | \"home\" | \"integrations\" | \"discover\" | \"observability-overview\" | \"streams\" | \"appSearch\" | \"dev_tools\" | \"maps\" | \"visualize\" | \"dev_tools:console\" | \"dev_tools:searchprofiler\" | \"dev_tools:painless_lab\" | \"dev_tools:grokdebugger\" | \"ml:notifications\" | \"ml:nodes\" | \"ml:overview\" | \"ml:memoryUsage\" | \"ml:settings\" | \"ml:dataVisualizer\" | \"ml:logPatternAnalysis\" | \"ml:logRateAnalysis\" | \"ml:singleMetricViewer\" | \"ml:anomalyDetection\" | \"ml:anomalyExplorer\" | \"ml:dataDrift\" | \"ml:dataFrameAnalytics\" | \"ml:resultExplorer\" | \"ml:analyticsMap\" | \"ml:aiOps\" | \"ml:changePointDetections\" | \"ml:modelManagement\" | \"ml:nodesOverview\" | \"ml:esqlDataVisualizer\" | \"ml:fileUpload\" | \"ml:indexDataVisualizer\" | \"ml:calendarSettings\" | \"ml:filterListsSettings\" | \"ml:suppliedConfigurations\" | \"osquery\" | \"management:transform\" | \"management:watcher\" | \"management:cases\" | \"management:tags\" | \"management:maintenanceWindows\" | \"management:cross_cluster_replication\" | \"management:dataViews\" | \"management:spaces\" | \"management:settings\" | \"management:users\" | \"management:migrate_data\" | \"management:search_sessions\" | \"management:data_quality\" | \"management:filesManagement\" | \"management:roles\" | \"management:reporting\" | \"management:aiAssistantManagementSelection\" | \"management:securityAiAssistantManagement\" | \"management:observabilityAiAssistantManagement\" | \"management:api_keys\" | \"management:license_management\" | \"management:index_lifecycle_management\" | \"management:index_management\" | \"management:ingest_pipelines\" | \"management:jobsListLink\" | \"management:objects\" | \"management:pipelines\" | \"management:remote_clusters\" | \"management:role_mappings\" | \"management:rollup_jobs\" | \"management:snapshot_restore\" | \"management:triggersActions\" | \"management:triggersActionsConnectors\" | \"management:upgrade_assistant\" | \"enterpriseSearch\" | \"enterpriseSearchContent\" | \"enterpriseSearchApplications\" | \"searchInferenceEndpoints\" | \"enterpriseSearchAnalytics\" | \"workplaceSearch\" | \"serverlessElasticsearch\" | \"serverlessConnectors\" | \"serverlessWebCrawlers\" | \"searchPlayground\" | \"searchHomepage\" | \"enterpriseSearchContent:connectors\" | \"enterpriseSearchContent:searchIndices\" | \"enterpriseSearchContent:webCrawlers\" | \"enterpriseSearchApplications:searchApplications\" | \"enterpriseSearchApplications:playground\" | \"appSearch:engines\" | \"searchInferenceEndpoints:inferenceEndpoints\" | \"elasticsearchStart\" | \"elasticsearchIndices\" | \"enterpriseSearchElasticsearch\" | \"enterpriseSearchVectorSearch\" | \"enterpriseSearchSemanticSearch\" | \"enterpriseSearchAISearch\" | \"elasticsearchIndices:createIndex\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"streams:overview\" | \"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:siem_migrations-rules\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\" | \"fleet:settings\" | \"fleet:agents\" | \"fleet:policies\" | \"fleet:data_streams\" | \"fleet:enrollment_tokens\" | \"fleet:uninstall_tokens\"" ], "path": "x-pack/plugins/search_indices/public/types.ts", "deprecated": false, diff --git a/api_docs/search_indices.mdx b/api_docs/search_indices.mdx index 0ce9640fe7623..b216966991247 100644 --- a/api_docs/search_indices.mdx +++ b/api_docs/search_indices.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchIndices title: "searchIndices" image: https://source.unsplash.com/400x175/?github description: API docs for the searchIndices plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchIndices'] --- import searchIndicesObj from './search_indices.devdocs.json'; diff --git a/api_docs/search_inference_endpoints.mdx b/api_docs/search_inference_endpoints.mdx index eafd7f17bb87f..5c485aa4fdf82 100644 --- a/api_docs/search_inference_endpoints.mdx +++ b/api_docs/search_inference_endpoints.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchInferenceEndpoints title: "searchInferenceEndpoints" image: https://source.unsplash.com/400x175/?github description: API docs for the searchInferenceEndpoints plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchInferenceEndpoints'] --- import searchInferenceEndpointsObj from './search_inference_endpoints.devdocs.json'; diff --git a/api_docs/search_navigation.devdocs.json b/api_docs/search_navigation.devdocs.json new file mode 100644 index 0000000000000..9f9ff7e4294c4 --- /dev/null +++ b/api_docs/search_navigation.devdocs.json @@ -0,0 +1,390 @@ +{ + "id": "searchNavigation", + "client": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem", + "type": "Interface", + "tags": [], + "label": "ClassicNavItem", + "description": [], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.datatestsubj", + "type": "string", + "tags": [], + "label": "'data-test-subj'", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.deepLink", + "type": "Object", + "tags": [], + "label": "deepLink", + "description": [], + "signature": [ + { + "pluginId": "searchNavigation", + "scope": "public", + "docId": "kibSearchNavigationPluginApi", + "section": "def-public.ClassicNavItemDeepLink", + "text": "ClassicNavItemDeepLink" + }, + " | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.iconToString", + "type": "string", + "tags": [], + "label": "iconToString", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.items", + "type": "Array", + "tags": [], + "label": "items", + "description": [], + "signature": [ + { + "pluginId": "searchNavigation", + "scope": "public", + "docId": "kibSearchNavigationPluginApi", + "section": "def-public.ClassicNavItem", + "text": "ClassicNavItem" + }, + "[] | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItem.name", + "type": "CompoundType", + "tags": [], + "label": "name", + "description": [], + "signature": [ + "string | number | boolean | React.ReactElement> | Iterable | React.ReactPortal | null | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItemDeepLink", + "type": "Interface", + "tags": [], + "label": "ClassicNavItemDeepLink", + "description": [], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItemDeepLink.link", + "type": "CompoundType", + "tags": [], + "label": "link", + "description": [], + "signature": [ + "\"fleet\" | \"graph\" | \"ml\" | \"monitoring\" | \"profiling\" | \"metrics\" | \"management\" | \"apm\" | \"synthetics\" | \"ux\" | \"canvas\" | \"logs\" | \"dashboards\" | \"slo\" | \"observabilityAIAssistant\" | \"home\" | \"integrations\" | \"discover\" | \"observability-overview\" | \"streams\" | \"appSearch\" | \"dev_tools\" | \"maps\" | \"visualize\" | \"dev_tools:console\" | \"dev_tools:searchprofiler\" | \"dev_tools:painless_lab\" | \"dev_tools:grokdebugger\" | \"ml:notifications\" | \"ml:nodes\" | \"ml:overview\" | \"ml:memoryUsage\" | \"ml:settings\" | \"ml:dataVisualizer\" | \"ml:logPatternAnalysis\" | \"ml:logRateAnalysis\" | \"ml:singleMetricViewer\" | \"ml:anomalyDetection\" | \"ml:anomalyExplorer\" | \"ml:dataDrift\" | \"ml:dataFrameAnalytics\" | \"ml:resultExplorer\" | \"ml:analyticsMap\" | \"ml:aiOps\" | \"ml:changePointDetections\" | \"ml:modelManagement\" | \"ml:nodesOverview\" | \"ml:esqlDataVisualizer\" | \"ml:fileUpload\" | \"ml:indexDataVisualizer\" | \"ml:calendarSettings\" | \"ml:filterListsSettings\" | \"ml:suppliedConfigurations\" | \"osquery\" | \"management:transform\" | \"management:watcher\" | \"management:cases\" | \"management:tags\" | \"management:maintenanceWindows\" | \"management:cross_cluster_replication\" | \"management:dataViews\" | \"management:spaces\" | \"management:settings\" | \"management:users\" | \"management:migrate_data\" | \"management:search_sessions\" | \"management:data_quality\" | \"management:filesManagement\" | \"management:roles\" | \"management:reporting\" | \"management:aiAssistantManagementSelection\" | \"management:securityAiAssistantManagement\" | \"management:observabilityAiAssistantManagement\" | \"management:api_keys\" | \"management:license_management\" | \"management:index_lifecycle_management\" | \"management:index_management\" | \"management:ingest_pipelines\" | \"management:jobsListLink\" | \"management:objects\" | \"management:pipelines\" | \"management:remote_clusters\" | \"management:role_mappings\" | \"management:rollup_jobs\" | \"management:snapshot_restore\" | \"management:triggersActions\" | \"management:triggersActionsConnectors\" | \"management:upgrade_assistant\" | \"enterpriseSearch\" | \"enterpriseSearchContent\" | \"enterpriseSearchApplications\" | \"searchInferenceEndpoints\" | \"enterpriseSearchAnalytics\" | \"workplaceSearch\" | \"serverlessElasticsearch\" | \"serverlessConnectors\" | \"serverlessWebCrawlers\" | \"searchPlayground\" | \"searchHomepage\" | \"enterpriseSearchContent:connectors\" | \"enterpriseSearchContent:searchIndices\" | \"enterpriseSearchContent:webCrawlers\" | \"enterpriseSearchApplications:searchApplications\" | \"enterpriseSearchApplications:playground\" | \"appSearch:engines\" | \"searchInferenceEndpoints:inferenceEndpoints\" | \"elasticsearchStart\" | \"elasticsearchIndices\" | \"enterpriseSearchElasticsearch\" | \"enterpriseSearchVectorSearch\" | \"enterpriseSearchSemanticSearch\" | \"enterpriseSearchAISearch\" | \"elasticsearchIndices:createIndex\" | \"observability-logs-explorer\" | \"last-used-logs-viewer\" | \"observabilityOnboarding\" | \"inventory\" | \"logs:settings\" | \"logs:stream\" | \"logs:log-categories\" | \"logs:anomalies\" | \"observability-overview:cases\" | \"observability-overview:alerts\" | \"observability-overview:rules\" | \"observability-overview:cases_create\" | \"observability-overview:cases_configure\" | \"metrics:settings\" | \"metrics:hosts\" | \"metrics:inventory\" | \"metrics:metrics-explorer\" | \"metrics:assetDetails\" | \"apm:services\" | \"apm:traces\" | \"apm:dependencies\" | \"apm:service-map\" | \"apm:settings\" | \"apm:service-groups-list\" | \"apm:storage-explorer\" | \"synthetics:overview\" | \"synthetics:certificates\" | \"profiling:functions\" | \"profiling:stacktraces\" | \"profiling:flamegraphs\" | \"inventory:datastreams\" | \"streams:overview\" | \"securitySolutionUI\" | \"securitySolutionUI:\" | \"securitySolutionUI:cases\" | \"securitySolutionUI:alerts\" | \"securitySolutionUI:rules\" | \"securitySolutionUI:policy\" | \"securitySolutionUI:overview\" | \"securitySolutionUI:dashboards\" | \"securitySolutionUI:kubernetes\" | \"securitySolutionUI:cases_create\" | \"securitySolutionUI:cases_configure\" | \"securitySolutionUI:hosts\" | \"securitySolutionUI:users\" | \"securitySolutionUI:cloud_defend-policies\" | \"securitySolutionUI:cloud_security_posture-dashboard\" | \"securitySolutionUI:cloud_security_posture-findings\" | \"securitySolutionUI:cloud_security_posture-benchmarks\" | \"securitySolutionUI:network\" | \"securitySolutionUI:data_quality\" | \"securitySolutionUI:explore\" | \"securitySolutionUI:assets\" | \"securitySolutionUI:cloud_defend\" | \"securitySolutionUI:notes\" | \"securitySolutionUI:administration\" | \"securitySolutionUI:attack_discovery\" | \"securitySolutionUI:blocklist\" | \"securitySolutionUI:cloud_security_posture-rules\" | \"securitySolutionUI:detections\" | \"securitySolutionUI:detection_response\" | \"securitySolutionUI:endpoints\" | \"securitySolutionUI:event_filters\" | \"securitySolutionUI:exceptions\" | \"securitySolutionUI:host_isolation_exceptions\" | \"securitySolutionUI:hosts-all\" | \"securitySolutionUI:hosts-anomalies\" | \"securitySolutionUI:hosts-risk\" | \"securitySolutionUI:hosts-events\" | \"securitySolutionUI:hosts-sessions\" | \"securitySolutionUI:hosts-uncommon_processes\" | \"securitySolutionUI:investigations\" | \"securitySolutionUI:get_started\" | \"securitySolutionUI:machine_learning-landing\" | \"securitySolutionUI:network-anomalies\" | \"securitySolutionUI:network-dns\" | \"securitySolutionUI:network-events\" | \"securitySolutionUI:network-flows\" | \"securitySolutionUI:network-http\" | \"securitySolutionUI:network-tls\" | \"securitySolutionUI:response_actions_history\" | \"securitySolutionUI:rules-add\" | \"securitySolutionUI:rules-create\" | \"securitySolutionUI:rules-landing\" | \"securitySolutionUI:siem_migrations-rules\" | \"securitySolutionUI:threat_intelligence\" | \"securitySolutionUI:timelines\" | \"securitySolutionUI:timelines-templates\" | \"securitySolutionUI:trusted_apps\" | \"securitySolutionUI:users-all\" | \"securitySolutionUI:users-anomalies\" | \"securitySolutionUI:users-authentications\" | \"securitySolutionUI:users-events\" | \"securitySolutionUI:users-risk\" | \"securitySolutionUI:entity_analytics\" | \"securitySolutionUI:entity_analytics-management\" | \"securitySolutionUI:entity_analytics-asset-classification\" | \"securitySolutionUI:entity_analytics-entity_store_management\" | \"securitySolutionUI:coverage-overview\" | \"fleet:settings\" | \"fleet:agents\" | \"fleet:policies\" | \"fleet:data_streams\" | \"fleet:enrollment_tokens\" | \"fleet:uninstall_tokens\"" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.ClassicNavItemDeepLink.shouldShowActiveForSubroutes", + "type": "CompoundType", + "tags": [], + "label": "shouldShowActiveForSubroutes", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginSetup", + "type": "Interface", + "tags": [], + "label": "SearchNavigationPluginSetup", + "description": [], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart", + "type": "Interface", + "tags": [], + "label": "SearchNavigationPluginStart", + "description": [], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.registerOnAppMountHandler", + "type": "Function", + "tags": [], + "label": "registerOnAppMountHandler", + "description": [], + "signature": [ + "(onAppMount: () => Promise) => void" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.registerOnAppMountHandler.$1", + "type": "Function", + "tags": [], + "label": "onAppMount", + "description": [], + "signature": [ + "() => Promise" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.handleOnAppMount", + "type": "Function", + "tags": [], + "label": "handleOnAppMount", + "description": [], + "signature": [ + "() => Promise" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.setGetBaseClassicNavItems", + "type": "Function", + "tags": [], + "label": "setGetBaseClassicNavItems", + "description": [], + "signature": [ + "(classicNavItemsFn: () => ", + { + "pluginId": "searchNavigation", + "scope": "public", + "docId": "kibSearchNavigationPluginApi", + "section": "def-public.ClassicNavItem", + "text": "ClassicNavItem" + }, + "[]) => void" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.setGetBaseClassicNavItems.$1", + "type": "Function", + "tags": [], + "label": "classicNavItemsFn", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "searchNavigation", + "scope": "public", + "docId": "kibSearchNavigationPluginApi", + "section": "def-public.ClassicNavItem", + "text": "ClassicNavItem" + }, + "[]" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.useClassicNavigation", + "type": "Function", + "tags": [], + "label": "useClassicNavigation", + "description": [], + "signature": [ + "(history: ", + { + "pluginId": "@kbn/core-application-browser", + "scope": "public", + "docId": "kibKbnCoreApplicationBrowserPluginApi", + "section": "def-public.ScopedHistory", + "text": "ScopedHistory" + }, + ") => ", + { + "pluginId": "@kbn/shared-ux-page-solution-nav", + "scope": "common", + "docId": "kibKbnSharedUxPageSolutionNavPluginApi", + "section": "def-common.SolutionNavProps", + "text": "SolutionNavProps" + }, + " | undefined" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "searchNavigation", + "id": "def-public.SearchNavigationPluginStart.useClassicNavigation.$1", + "type": "Object", + "tags": [], + "label": "history", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-application-browser", + "scope": "public", + "docId": "kibKbnCoreApplicationBrowserPluginApi", + "section": "def-public.ScopedHistory", + "text": "ScopedHistory" + }, + "" + ], + "path": "x-pack/plugins/search_solution/search_navigation/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "searchNavigation", + "id": "def-common.PLUGIN_ID", + "type": "string", + "tags": [], + "label": "PLUGIN_ID", + "description": [], + "signature": [ + "\"searchNavigation\"" + ], + "path": "x-pack/plugins/search_solution/search_navigation/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "searchNavigation", + "id": "def-common.PLUGIN_NAME", + "type": "string", + "tags": [], + "label": "PLUGIN_NAME", + "description": [], + "signature": [ + "\"searchNavigation\"" + ], + "path": "x-pack/plugins/search_solution/search_navigation/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/search_navigation.mdx b/api_docs/search_navigation.mdx new file mode 100644 index 0000000000000..b748885ca4e8e --- /dev/null +++ b/api_docs/search_navigation.mdx @@ -0,0 +1,41 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibSearchNavigationPluginApi +slug: /kibana-dev-docs/api/searchNavigation +title: "searchNavigation" +image: https://source.unsplash.com/400x175/?github +description: API docs for the searchNavigation plugin +date: 2024-11-26 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchNavigation'] +--- +import searchNavigationObj from './search_navigation.devdocs.json'; + + + +Contact [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 21 | 0 | 21 | 0 | + +## Client + +### Setup + + +### Start + + +### Interfaces + + +## Common + +### Consts, variables and types + + diff --git a/api_docs/search_notebooks.mdx b/api_docs/search_notebooks.mdx index 49b4a6ced27b5..de74e78045c7a 100644 --- a/api_docs/search_notebooks.mdx +++ b/api_docs/search_notebooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchNotebooks title: "searchNotebooks" image: https://source.unsplash.com/400x175/?github description: API docs for the searchNotebooks plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchNotebooks'] --- import searchNotebooksObj from './search_notebooks.devdocs.json'; diff --git a/api_docs/search_playground.mdx b/api_docs/search_playground.mdx index a2f9128eeb984..4830ad3ee8160 100644 --- a/api_docs/search_playground.mdx +++ b/api_docs/search_playground.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchPlayground title: "searchPlayground" image: https://source.unsplash.com/400x175/?github description: API docs for the searchPlayground plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchPlayground'] --- import searchPlaygroundObj from './search_playground.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index beced7ef042f4..70f8c0209b173 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index f62340cda6832..f76d77c7255ef 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -880,8 +880,8 @@ "pluginId": "timelines", "scope": "common", "docId": "kibTimelinesPluginApi", - "section": "def-common.EqlOptionsSelected", - "text": "EqlOptionsSelected" + "section": "def-common.EqlOptions", + "text": "EqlOptions" } ], "path": "x-pack/plugins/security_solution/public/timelines/store/model.ts", @@ -3267,7 +3267,7 @@ "\nA list of allowed values that can be used in `xpack.securitySolution.enableExperimental`.\nThis object is then used to validate and parse the value entered." ], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; readonly defendInsights: false; }" + "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: false; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; readonly defendInsights: false; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index fe996ca8192c7..d074e246f4cbd 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/security_solution_ess.mdx b/api_docs/security_solution_ess.mdx index d3702f31a2a09..155a6860ec2bb 100644 --- a/api_docs/security_solution_ess.mdx +++ b/api_docs/security_solution_ess.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionEss title: "securitySolutionEss" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionEss plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionEss'] --- import securitySolutionEssObj from './security_solution_ess.devdocs.json'; diff --git a/api_docs/security_solution_serverless.mdx b/api_docs/security_solution_serverless.mdx index da76bda855ad2..6f773dcd51a54 100644 --- a/api_docs/security_solution_serverless.mdx +++ b/api_docs/security_solution_serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionServerless title: "securitySolutionServerless" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionServerless plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionServerless'] --- import securitySolutionServerlessObj from './security_solution_serverless.devdocs.json'; diff --git a/api_docs/serverless.mdx b/api_docs/serverless.mdx index 9ed3edf484f4e..4733779647065 100644 --- a/api_docs/serverless.mdx +++ b/api_docs/serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverless title: "serverless" image: https://source.unsplash.com/400x175/?github description: API docs for the serverless plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverless'] --- import serverlessObj from './serverless.devdocs.json'; diff --git a/api_docs/serverless_observability.mdx b/api_docs/serverless_observability.mdx index ca029505d397b..e4d126dc1774b 100644 --- a/api_docs/serverless_observability.mdx +++ b/api_docs/serverless_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessObservability title: "serverlessObservability" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessObservability plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessObservability'] --- import serverlessObservabilityObj from './serverless_observability.devdocs.json'; diff --git a/api_docs/serverless_search.mdx b/api_docs/serverless_search.mdx index c69eda54adde7..2b4b60dc53e14 100644 --- a/api_docs/serverless_search.mdx +++ b/api_docs/serverless_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessSearch title: "serverlessSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessSearch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessSearch'] --- import serverlessSearchObj from './serverless_search.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 621edaab4fed3..a82291e9e282f 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 18739a7c29ae1..b5618512cfd70 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/slo.mdx b/api_docs/slo.mdx index 0e3f28446c7f8..03fcb2e0efdca 100644 --- a/api_docs/slo.mdx +++ b/api_docs/slo.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/slo title: "slo" image: https://source.unsplash.com/400x175/?github description: API docs for the slo plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'slo'] --- import sloObj from './slo.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 9aaee9101af97..3218b67bfa882 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 82fef50bf0bdd..2c97f988ca03c 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 93ec820314269..7c5312908e7ce 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index cf41a04195d6d..a361cc1edd6b6 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/streams.devdocs.json b/api_docs/streams.devdocs.json index 02800505b0823..1d744b0580b2f 100644 --- a/api_docs/streams.devdocs.json +++ b/api_docs/streams.devdocs.json @@ -6,12 +6,323 @@ "interfaces": [], "enums": [], "misc": [], - "objects": [] + "objects": [], + "setup": { + "parentPluginId": "streams", + "id": "def-public.StreamsPluginSetup", + "type": "Interface", + "tags": [], + "label": "StreamsPluginSetup", + "description": [], + "path": "x-pack/plugins/streams/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "streams", + "id": "def-public.StreamsPluginSetup.status$", + "type": "Object", + "tags": [], + "label": "status$", + "description": [], + "signature": [ + "Observable", + "<{ status: \"unknown\" | \"disabled\" | \"enabled\"; }>" + ], + "path": "x-pack/plugins/streams/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "streams", + "id": "def-public.StreamsPluginStart", + "type": "Interface", + "tags": [], + "label": "StreamsPluginStart", + "description": [], + "path": "x-pack/plugins/streams/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "streams", + "id": "def-public.StreamsPluginStart.streamsRepositoryClient", + "type": "Object", + "tags": [], + "label": "streamsRepositoryClient", + "description": [], + "signature": [ + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.RouteRepositoryClient", + "text": "RouteRepositoryClient" + }, + "<{ \"POST /api/streams/_disable\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /api/streams/_disable\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; \"POST /internal/streams/esql\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /internal/streams/esql\", Zod.ZodObject<{ body: Zod.ZodObject<{ query: Zod.ZodString; operationName: Zod.ZodString; filter: Zod.ZodOptional, Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\">>>; kuery: Zod.ZodOptional; start: Zod.ZodOptional; end: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectOutputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }, { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectOutputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }; }, { body: { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }; }>, ", + "StreamsRouteHandlerResources", + ", ", + "UnparsedEsqlResponse", + ", undefined>; \"GET /api/streams/_status\": { endpoint: \"GET /api/streams/_status\"; handler: ServerRouteHandler<", + "StreamsRouteHandlerResources", + ", undefined, { enabled: boolean; }>; security?: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "server", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" + }, + " | undefined; }; \"GET /api/streams\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/streams\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "StreamsRouteHandlerResources", + ", { definitions: { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }[]; trees: ", + "StreamTree", + "[]; }, undefined>; \"DELETE /api/streams/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"DELETE /api/streams/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; \"PUT /api/streams/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PUT /api/streams/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject<{ processing: Zod.ZodDefault>; config: Zod.ZodDiscriminatedUnion<\"type\", [Zod.ZodObject<{ type: Zod.ZodLiteral<\"grok\">; field: Zod.ZodString; patterns: Zod.ZodArray; pattern_definitions: Zod.ZodOptional>; }, \"strip\", Zod.ZodTypeAny, { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; }, { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"dissect\">; field: Zod.ZodString; pattern: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { type: \"dissect\"; field: string; pattern: string; }, { type: \"dissect\"; field: string; pattern: string; }>]>; }, \"strip\", Zod.ZodTypeAny, { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }, { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }>, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", + "Condition", + "; }, { id: string; condition?: ", + "Condition", + "; }>, \"many\">>; }, \"strip\", Zod.ZodTypeAny, { children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }, { children?: { id: string; condition?: ", + "Condition", + "; }[] | undefined; fields?: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[] | undefined; processing?: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[] | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; body: { children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }; }, { path: { id: string; }; body: { children?: { id: string; condition?: ", + "Condition", + "; }[] | undefined; fields?: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[] | undefined; processing?: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[] | undefined; }; }>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: boolean; }, undefined>; \"GET /api/streams/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/streams/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "StreamsRouteHandlerResources", + ", { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; } & { inheritedFields: ({ type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; } & { from: string; })[]; }, undefined>; \"POST /api/streams/{id}/_fork\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /api/streams/{id}/_fork\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject<{ stream: Zod.ZodObject>; config: Zod.ZodDiscriminatedUnion<\"type\", [Zod.ZodObject<{ type: Zod.ZodLiteral<\"grok\">; field: Zod.ZodString; patterns: Zod.ZodArray; pattern_definitions: Zod.ZodOptional>; }, \"strip\", Zod.ZodTypeAny, { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; }, { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; }>, Zod.ZodObject<{ type: Zod.ZodLiteral<\"dissect\">; field: Zod.ZodString; pattern: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { type: \"dissect\"; field: string; pattern: string; }, { type: \"dissect\"; field: string; pattern: string; }>]>; }, \"strip\", Zod.ZodTypeAny, { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }, { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }>, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault>; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", + "Condition", + "; }, { id: string; condition?: ", + "Condition", + "; }>, \"many\">>; }, { id: Zod.ZodString; }>, \"children\">, \"strip\", Zod.ZodTypeAny, { id: string; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }, { id: string; fields?: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[] | undefined; processing?: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[] | undefined; }>; condition: Zod.ZodType<", + "Condition", + ", Zod.ZodTypeDef, ", + "Condition", + ">; }, \"strip\", Zod.ZodTypeAny, { stream: { id: string; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }; condition?: ", + "Condition", + "; }, { stream: { id: string; fields?: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[] | undefined; processing?: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[] | undefined; }; condition?: ", + "Condition", + "; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; body: { stream: { id: string; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }; condition?: ", + "Condition", + "; }; }, { path: { id: string; }; body: { stream: { id: string; fields?: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[] | undefined; processing?: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[] | undefined; }; condition?: ", + "Condition", + "; }; }>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; \"POST /api/streams/_resync\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /api/streams/_resync\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; \"POST /api/streams/_enable\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"POST /api/streams/_enable\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; }, ", + "StreamsRepositoryClientOptions", + ">" + ], + "path": "x-pack/plugins/streams/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "streams", + "id": "def-public.StreamsPluginStart.status$", + "type": "Object", + "tags": [], + "label": "status$", + "description": [], + "signature": [ + "Observable", + "<{ status: \"unknown\" | \"disabled\" | \"enabled\"; }>" + ], + "path": "x-pack/plugins/streams/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "lifecycle": "start", + "initialIsOpen": true + } }, "server": { "classes": [], "functions": [], - "interfaces": [], + "interfaces": [ + { + "parentPluginId": "streams", + "id": "def-server.ListStreamResponse", + "type": "Interface", + "tags": [], + "label": "ListStreamResponse", + "description": [], + "path": "x-pack/plugins/streams/server/lib/streams/stream_crud.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "streams", + "id": "def-server.ListStreamResponse.total", + "type": "number", + "tags": [], + "label": "total", + "description": [], + "path": "x-pack/plugins/streams/server/lib/streams/stream_crud.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "streams", + "id": "def-server.ListStreamResponse.definitions", + "type": "Array", + "tags": [], + "label": "definitions", + "description": [], + "signature": [ + "{ id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }[]" + ], + "path": "x-pack/plugins/streams/server/lib/streams/stream_crud.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], "enums": [], "misc": [ { @@ -37,7 +348,7 @@ "label": "StreamsRouteRepository", "description": [], "signature": [ - "{ \"GET /api/streams 2023-10-31\": ", + "{ \"POST /api/streams/_disable\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -45,25 +356,9 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/streams 2023-10-31\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "<\"POST /api/streams/_disable\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"DELETE /api/streams/{id} 2023-10-31\": ", + ", { acknowledged: true; }, undefined>; \"POST /internal/streams/esql\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -71,25 +366,37 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"DELETE /api/streams/{id} 2023-10-31\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "<\"POST /internal/streams/esql\", Zod.ZodObject<{ body: Zod.ZodObject<{ query: Zod.ZodString; operationName: Zod.ZodString; filter: Zod.ZodOptional, Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\">>>; kuery: Zod.ZodOptional; start: Zod.ZodOptional; end: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectOutputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }, { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }>; }, \"strip\", Zod.ZodTypeAny, { body: { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectOutputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }; }, { body: { query: string; operationName: string; start?: number | undefined; end?: number | undefined; filter?: Zod.objectInputType<{}, Zod.ZodTypeAny, \"passthrough\"> | undefined; kuery?: string | undefined; }; }>, ", "StreamsRouteHandlerResources", ", ", + "UnparsedEsqlResponse", + ", undefined>; \"GET /api/streams/_status\": { endpoint: \"GET /api/streams/_status\"; handler: ServerRouteHandler<", + "StreamsRouteHandlerResources", + ", undefined, { enabled: boolean; }>; security?: ", { "pluginId": "@kbn/core-http-server", "scope": "server", "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" + "section": "def-server.RouteSecurity", + "text": "RouteSecurity" }, - ", ", + " | undefined; }; \"GET /api/streams\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" + "section": "def-common.ServerRoute", + "text": "ServerRoute" }, - ">; \"PUT /api/streams/{id} 2023-10-31\": ", + "<\"GET /api/streams\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "StreamsRouteHandlerResources", + ", { definitions: { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }[]; trees: ", + "StreamTree", + "[]; }, undefined>; \"DELETE /api/streams/{id}\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -97,7 +404,17 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"PUT /api/streams/{id} 2023-10-31\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject<{ processing: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "StreamsRouteHandlerResources", + ", { acknowledged: true; }, undefined>; \"PUT /api/streams/{id}\": ", + { + "pluginId": "@kbn/server-route-repository-utils", + "scope": "common", + "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", + "section": "def-common.ServerRoute", + "text": "ServerRoute" + }, + "<\"PUT /api/streams/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject<{ processing: Zod.ZodDefault | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", "Condition", - "; }>, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", + ">>; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", "Condition", "; }, { id: string; condition?: ", "Condition", @@ -131,23 +448,7 @@ "Condition", "; }[] | undefined; }; }>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"GET /api/streams/{id} 2023-10-31\": ", + ", { acknowledged: boolean; }, undefined>; \"GET /api/streams/{id}\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -155,25 +456,13 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/streams/{id} 2023-10-31\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", + "<\"GET /api/streams/{id}\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; }, \"strip\", Zod.ZodTypeAny, { path: { id: string; }; }, { path: { id: string; }; }>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"POST /api/streams/{id}/_fork 2023-10-31\": ", + ", { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; } & { inheritedFields: ({ type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; } & { from: string; })[]; }, undefined>; \"POST /api/streams/{id}/_fork\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -181,7 +470,7 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/streams/{id}/_fork 2023-10-31\", Zod.ZodObject<{ path: Zod.ZodObject<{ id: Zod.ZodString; }, \"strip\", Zod.ZodTypeAny, { id: string; }, { id: string; }>; body: Zod.ZodObject<{ stream: Zod.ZodObject; body: Zod.ZodObject<{ stream: Zod.ZodObject | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", "Condition", - "; }>, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault, \"many\">>; fields: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }, { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }>, \"many\">>; children: Zod.ZodDefault; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", + ">>; }, \"strip\", Zod.ZodTypeAny, { id: string; condition?: ", "Condition", "; }, { id: string; condition?: ", "Condition", @@ -223,23 +512,7 @@ "Condition", "; }; }>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"POST /api/streams/_resync 2023-10-31\": ", + ", { acknowledged: true; }, undefined>; \"POST /api/streams/_resync\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -247,25 +520,9 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/streams/_resync 2023-10-31\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "<\"POST /api/streams/_resync\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; \"POST /api/streams/_enable 2023-10-31\": ", + ", { acknowledged: true; }, undefined>; \"POST /api/streams/_enable\": ", { "pluginId": "@kbn/server-route-repository-utils", "scope": "common", @@ -273,25 +530,9 @@ "section": "def-common.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/streams/_enable 2023-10-31\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", + "<\"POST /api/streams/_enable\", Zod.ZodObject<{}, \"strip\", Zod.ZodTypeAny, {}, {}>, ", "StreamsRouteHandlerResources", - ", ", - { - "pluginId": "@kbn/core-http-server", - "scope": "server", - "docId": "kibKbnCoreHttpServerPluginApi", - "section": "def-server.IKibanaResponse", - "text": "IKibanaResponse" - }, - ", ", - { - "pluginId": "@kbn/server-route-repository-utils", - "scope": "common", - "docId": "kibKbnServerRouteRepositoryUtilsPluginApi", - "section": "def-common.DefaultRouteCreateOptions", - "text": "DefaultRouteCreateOptions" - }, - ">; }" + ", { acknowledged: true; }, undefined>; }" ], "path": "x-pack/plugins/streams/server/routes/index.ts", "deprecated": false, @@ -299,120 +540,7 @@ "initialIsOpen": false } ], - "objects": [ - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository", - "type": "Object", - "tags": [], - "label": "StreamsRouteRepository", - "description": [], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "streams", - "id": "def-server.StreamsRouteRepository.Unnamed", - "type": "Any", - "tags": [], - "label": "Unnamed", - "description": [], - "signature": [ - "any" - ], - "path": "x-pack/plugins/streams/server/routes/index.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], + "objects": [], "setup": { "parentPluginId": "streams", "id": "def-server.StreamsPluginSetup", @@ -447,7 +575,27 @@ "functions": [], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "streams", + "id": "def-common.StreamDefinition", + "type": "Type", + "tags": [], + "label": "StreamDefinition", + "description": [], + "signature": [ + "{ id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }" + ], + "path": "x-pack/plugins/streams/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [] } } \ No newline at end of file diff --git a/api_docs/streams.mdx b/api_docs/streams.mdx index 61dc95c44fddc..711ba66f71ab0 100644 --- a/api_docs/streams.mdx +++ b/api_docs/streams.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/streams title: "streams" image: https://source.unsplash.com/400x175/?github description: API docs for the streams plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'streams'] --- import streamsObj from './streams.devdocs.json'; @@ -21,7 +21,15 @@ Contact @simianhacker @flash1293 @dgieselaar for questions regarding this plugin | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 12 | 7 | 12 | 2 | +| 13 | 0 | 13 | 4 | + +## Client + +### Setup + + +### Start + ## Server @@ -31,9 +39,14 @@ Contact @simianhacker @flash1293 @dgieselaar for questions regarding this plugin ### Start -### Objects - +### Interfaces + ### Consts, variables and types +## Common + +### Consts, variables and types + + diff --git a/api_docs/streams_app.devdocs.json b/api_docs/streams_app.devdocs.json new file mode 100644 index 0000000000000..2bb7bd48fb0cb --- /dev/null +++ b/api_docs/streams_app.devdocs.json @@ -0,0 +1,148 @@ +{ + "id": "streamsApp", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "streamsApp", + "id": "def-public.StreamsAppPublicSetup", + "type": "Interface", + "tags": [], + "label": "StreamsAppPublicSetup", + "description": [], + "path": "x-pack/plugins/streams_app/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "streamsApp", + "id": "def-public.StreamsAppPublicStart", + "type": "Interface", + "tags": [], + "label": "StreamsAppPublicStart", + "description": [], + "path": "x-pack/plugins/streams_app/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "streamsApp", + "id": "def-server.StreamsAppServerSetup", + "type": "Interface", + "tags": [], + "label": "StreamsAppServerSetup", + "description": [], + "path": "x-pack/plugins/streams_app/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "streamsApp", + "id": "def-server.StreamsAppServerStart", + "type": "Interface", + "tags": [], + "label": "StreamsAppServerStart", + "description": [], + "path": "x-pack/plugins/streams_app/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "streamsApp", + "id": "def-common.EntityTypeDefinition", + "type": "Interface", + "tags": [], + "label": "EntityTypeDefinition", + "description": [], + "path": "x-pack/plugins/streams_app/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "streamsApp", + "id": "def-common.EntityTypeDefinition.displayName", + "type": "string", + "tags": [], + "label": "displayName", + "description": [], + "path": "x-pack/plugins/streams_app/common/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "streamsApp", + "id": "def-common.Entity", + "type": "Type", + "tags": [], + "label": "Entity", + "description": [], + "signature": [ + "EntityBase & { type: \"stream\"; properties: { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }; }" + ], + "path": "x-pack/plugins/streams_app/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "streamsApp", + "id": "def-common.StreamEntity", + "type": "Type", + "tags": [], + "label": "StreamEntity", + "description": [], + "signature": [ + "EntityBase & { type: \"stream\"; properties: { id: string; children: { id: string; condition?: ", + "Condition", + "; }[]; fields: { type: \"boolean\" | \"ip\" | \"keyword\" | \"date\" | \"long\" | \"double\" | \"match_only_text\"; name: string; }[]; processing: { config: { type: \"grok\"; field: string; patterns: string[]; pattern_definitions?: Record | undefined; } | { type: \"dissect\"; field: string; pattern: string; }; condition?: ", + "Condition", + "; }[]; }; }" + ], + "path": "x-pack/plugins/streams_app/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/streams_app.mdx b/api_docs/streams_app.mdx new file mode 100644 index 0000000000000..1a91877bccb03 --- /dev/null +++ b/api_docs/streams_app.mdx @@ -0,0 +1,49 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibStreamsAppPluginApi +slug: /kibana-dev-docs/api/streamsApp +title: "streamsApp" +image: https://source.unsplash.com/400x175/?github +description: API docs for the streamsApp plugin +date: 2024-11-26 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'streamsApp'] +--- +import streamsAppObj from './streams_app.devdocs.json'; + + + +Contact @simianhacker @flash1293 @dgieselaar for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 8 | 0 | 8 | 0 | + +## Client + +### Setup + + +### Start + + +## Server + +### Setup + + +### Start + + +## Common + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index eb637b28265b9..1aa0629eaac5e 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index f9df55ffe063e..d11feca183d97 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index 312c866bf5237..52bea13e8a621 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 92f25b5d73054..cef1f9e119e0c 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 60e8abc571190..141a225a8a666 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 0904b1d8c25ab..c404e4317d82e 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -1465,7 +1465,181 @@ }, "common": { "classes": [], - "functions": [], + "functions": [ + { + "parentPluginId": "timelines", + "id": "def-common.getDataFromFieldsHits", + "type": "Function", + "tags": [], + "label": "getDataFromFieldsHits", + "description": [], + "signature": [ + "(fields: ", + "Fields", + ", prependField?: string | undefined, prependFieldCategory?: string | undefined) => ", + { + "pluginId": "timelines", + "scope": "common", + "docId": "kibTimelinesPluginApi", + "section": "def-common.TimelineEventsDetailsItem", + "text": "TimelineEventsDetailsItem" + }, + "[]" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-common.getDataFromFieldsHits.$1", + "type": "Object", + "tags": [], + "label": "fields", + "description": [], + "signature": [ + "Fields", + "" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "timelines", + "id": "def-common.getDataFromFieldsHits.$2", + "type": "string", + "tags": [], + "label": "prependField", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.getDataFromFieldsHits.$3", + "type": "string", + "tags": [], + "label": "prependFieldCategory", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.isGeoField", + "type": "Function", + "tags": [], + "label": "isGeoField", + "description": [], + "signature": [ + "(field: string) => boolean" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-common.isGeoField.$1", + "type": "string", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/timelines/common/utils/field_formatters.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.toArray", + "type": "Function", + "tags": [], + "label": "toArray", + "description": [], + "signature": [ + "(value: T | T[] | null | undefined) => T[]" + ], + "path": "x-pack/plugins/timelines/common/utils/to_array.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-common.toArray.$1", + "type": "CompoundType", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "T | T[] | null | undefined" + ], + "path": "x-pack/plugins/timelines/common/utils/to_array.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "timelines", + "id": "def-common.toObjectArrayOfStrings", + "type": "Function", + "tags": [], + "label": "toObjectArrayOfStrings", + "description": [], + "signature": [ + "(value: T | T[] | null) => { str: string; isObjectArray?: boolean | undefined; }[]" + ], + "path": "x-pack/plugins/timelines/common/utils/to_array.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "timelines", + "id": "def-common.toObjectArrayOfStrings.$1", + "type": "CompoundType", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "T | T[] | null" + ], + "path": "x-pack/plugins/timelines/common/utils/to_array.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], "interfaces": [ { "parentPluginId": "timelines", @@ -1843,10 +2017,10 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsData", + "id": "def-common.EqlFieldsComboBoxOptions", "type": "Interface", "tags": [], - "label": "EqlOptionsData", + "label": "EqlFieldsComboBoxOptions", "description": [], "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", "deprecated": false, @@ -1854,7 +2028,7 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsData.keywordFields", + "id": "def-common.EqlFieldsComboBoxOptions.keywordFields", "type": "Array", "tags": [], "label": "keywordFields", @@ -1869,7 +2043,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsData.dateFields", + "id": "def-common.EqlFieldsComboBoxOptions.dateFields", "type": "Array", "tags": [], "label": "dateFields", @@ -1884,7 +2058,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsData.nonDateFields", + "id": "def-common.EqlFieldsComboBoxOptions.nonDateFields", "type": "Array", "tags": [], "label": "nonDateFields", @@ -1902,10 +2076,10 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected", + "id": "def-common.EqlOptions", "type": "Interface", "tags": [], - "label": "EqlOptionsSelected", + "label": "EqlOptions", "description": [], "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", "deprecated": false, @@ -1913,7 +2087,7 @@ "children": [ { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.eventCategoryField", + "id": "def-common.EqlOptions.eventCategoryField", "type": "string", "tags": [], "label": "eventCategoryField", @@ -1927,7 +2101,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.tiebreakerField", + "id": "def-common.EqlOptions.tiebreakerField", "type": "string", "tags": [], "label": "tiebreakerField", @@ -1941,7 +2115,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.timestampField", + "id": "def-common.EqlOptions.timestampField", "type": "string", "tags": [], "label": "timestampField", @@ -1955,7 +2129,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.query", + "id": "def-common.EqlOptions.query", "type": "string", "tags": [], "label": "query", @@ -1969,7 +2143,7 @@ }, { "parentPluginId": "timelines", - "id": "def-common.EqlOptionsSelected.size", + "id": "def-common.EqlOptions.size", "type": "number", "tags": [], "label": "size", @@ -4143,8 +4317,8 @@ "pluginId": "timelines", "scope": "common", "docId": "kibTimelinesPluginApi", - "section": "def-common.EqlOptionsSelected", - "text": "EqlOptionsSelected" + "section": "def-common.EqlOptions", + "text": "EqlOptions" } ], "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts", diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 6ae93884c4c23..88f7e01a3eec2 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-threat-hunting-investigations](https://github.com/org | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 226 | 1 | 182 | 17 | +| 236 | 1 | 192 | 18 | ## Client @@ -50,6 +50,9 @@ Contact [@elastic/security-threat-hunting-investigations](https://github.com/org ### Objects +### Functions + + ### Interfaces diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index d374a68bd97ea..0e972db1e2298 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 1b7bc8b02715a..401e974737e77 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 7527fb3a69882..9d126442020dc 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 0d133d56b7a35..54888f409d0c2 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_doc_viewer.mdx b/api_docs/unified_doc_viewer.mdx index 17e877e7461d0..8619114a71693 100644 --- a/api_docs/unified_doc_viewer.mdx +++ b/api_docs/unified_doc_viewer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedDocViewer title: "unifiedDocViewer" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedDocViewer plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedDocViewer'] --- import unifiedDocViewerObj from './unified_doc_viewer.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index be8dd17292a2c..4b186ad113eba 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 8cfad5be3f003..7cf003158cdde 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 2d63214a6f4bc..862dbba5825df 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/uptime.mdx b/api_docs/uptime.mdx index 0c45c37bb221c..afeb97f9a2346 100644 --- a/api_docs/uptime.mdx +++ b/api_docs/uptime.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uptime title: "uptime" image: https://source.unsplash.com/400x175/?github description: API docs for the uptime plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uptime'] --- import uptimeObj from './uptime.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index f83cd5d6ae9ae..d290f15b8d29d 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 7a63eeb0e4a73..82ca2063d3474 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index df78cd34de7aa..ca1c1c544650a 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 26941d1ddf058..e21c0b14aff64 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 25a5834dede7d..2a95c87e774dd 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 5adb18dc78c46..e10792c5d7e68 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index d93490fb31ac7..6cad51ffd9cfc 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index 1041603461f1c..f749b43b6da1d 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 9d8810c19098e..6c8c83062bb24 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 11d22d4009301..8b11a9920127b 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index d093b71336e6c..a86e7396aab6f 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 63f32e78862a7..b1ff41b15eb94 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 7304a38526b4c..4a38efe31c722 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 74138f0c8889f..831e757cf5abc 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2024-11-22 +date: 2024-11-26 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/config/serverless.es.yml b/config/serverless.es.yml index eafa7f3113395..4b0d416bacdfc 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -113,7 +113,7 @@ data_visualizer.resultLinks.fileBeat.enabled: false xpack.searchPlayground.ui.enabled: true # Search InferenceEndpoints -xpack.searchInferenceEndpoints.ui.enabled: false +xpack.searchInferenceEndpoints.ui.enabled: true # Search Notebooks xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json diff --git a/config/serverless.yml b/config/serverless.yml index 0967df966f61a..d62f26a4642eb 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -229,3 +229,8 @@ monitoring.ui.enabled: false xpack.securitySolution.enableUiSettingsValidations: true data.enableUiSettingsValidations: true discover.enableUiSettingsValidations: true + +## Data Usage in stack management +xpack.dataUsage.enabled: true +# This feature is disabled in Serverless until fully tested within a Serverless environment +xpack.dataUsage.enableExperimental: ['dataUsageDisabled'] diff --git a/dev_docs/tutorials/generating_oas_for_http_apis.mdx b/dev_docs/tutorials/generating_oas_for_http_apis.mdx index 19852206f8006..f0f497c4286f1 100644 --- a/dev_docs/tutorials/generating_oas_for_http_apis.mdx +++ b/dev_docs/tutorials/generating_oas_for_http_apis.mdx @@ -57,7 +57,9 @@ Other useful query parameters for filtering are: import { schema, TypeOf } from '@kbn/config-schema'; export const fooResource = schema.object({ - name: schema.string() + name: schema.string({ + meta: { description: 'A unique identifier for...' }, + }), // ...and any other fields you may need }); @@ -114,8 +116,14 @@ function registerFooRoute(router: IRouter, docLinks: DoclinksStart) { access: 'public', summary: 'Create a foo resource' description: `A foo resource enables baz. See the following [documentation](${docLinks.links.fooResource}).`, - tags: ['oas-tag:my tag'], // Each operation must have a tag that's used to group similar endpoints in the docs - deprecated: true // An indicator that the operation is deprecated + deprecated: true, // An indicator that the operation is deprecated + options: { + tags: ['oas-tag:my tag'], // Each operation must have a tag that's used to group similar endpoints in the docs + availability: { + since: '1.0.0', + stability: 'experimental', + }, + }, }) .addVersion({ version: '2023-10-31', diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index ea31863576115..8e8ee80ff81be 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -579,6 +579,10 @@ security and spaces filtering. |This plugin provides access to observed entity data, such as information about hosts, pods, containers, services, and more. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/entity_manager_app/README.md[entityManagerApp] +|This plugin provides a user interface to interact with the Entity Manager. + + |{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] |The event log plugin provides a persistent history of alerting and action activities. @@ -832,6 +836,10 @@ It uses Chromium and Puppeteer underneath to run the browser in headless mode. |The Inference Endpoints is a tool used to manage inference endpoints +|{kib-repo}blob/{branch}/x-pack/plugins/search_solution/search_navigation/README.mdx[searchNavigation] +|The Search Navigation plugin is used to handle navigation for search solution plugins across both stack and serverless. + + |{kib-repo}blob/{branch}/x-pack/plugins/search_notebooks/README.mdx[searchNotebooks] |This plugin contains endpoints and components for rendering search python notebooks in the persistent dev console. @@ -909,6 +917,10 @@ routes, etc. |This plugin provides an interface to manage streams +|{kib-repo}blob/{branch}/x-pack/plugins/streams_app/README.md[streamsApp] +|Home of the Streams app plugin, which allows users to manage Streams via the UI. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/synthetics/README.md[synthetics] |The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening in their infrastructure. diff --git a/docs/discover/document-explorer.asciidoc b/docs/discover/document-explorer.asciidoc index 921e0504f4596..47b4a5bc3fcfd 100644 --- a/docs/discover/document-explorer.asciidoc +++ b/docs/discover/document-explorer.asciidoc @@ -29,7 +29,7 @@ image:images/discover-customize-table.png[Options to customize the table in Disc [[document-explorer-columns]] ==== Reorder and resize the columns -* To move a single column, open the column's contextual options, and select *Move left* or *Move right* in the available options. +* To move a single column, drag its header and drop it to the position you want. You can also open the column's contextual options, and select *Move left* or *Move right* in the available options. * To move multiple columns, click *Columns*. In the pop-up, drag the column names to their new order. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 49aa22de9fd35..b43f3b268e438 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -302,11 +302,5 @@ Enables a check that warns you when there's a potential formula included in the `xpack.reporting.csv.escapeFormulaValues`:: Escape formula values in cells with a `'`. See OWASP: https://www.owasp.org/index.php/CSV_Injection. Defaults to `true`. -`xpack.reporting.csv.enablePanelActionDownload`:: -deprecated:[8.14.0,This setting will be removed in an upcoming version of {kib}.] When `true`, this -setting enables a deprecated feature which allows users to download a CSV export from a saved search -panel on a dashboard. When `false`, users can generate regular CSV reports from a saved search panel on a -dashboard and later download them in *Stack Management > Reporting*. Defaults to `false`. - `xpack.reporting.csv.useByteOrderMarkEncoding`:: Adds a byte order mark (`\ufeff`) at the beginning of the CSV file. Defaults to `false`. diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 9b446eacf7bb9..b96aaa3a3ce00 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -6919,6 +6919,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -7943,6 +7949,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -8736,6 +8748,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -9790,6 +9808,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -10813,6 +10837,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -11607,6 +11637,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -12744,6 +12780,22 @@ ] } }, + { + "in": "query", + "name": "pkgName", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pkgVersion", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "query", "name": "previewData", @@ -30076,6 +30128,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -30563,6 +30621,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -30800,6 +30864,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -31336,6 +31406,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -32038,6 +32114,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33210,6 +33292,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33622,6 +33710,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -34314,6 +34408,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -34808,6 +34908,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -35044,6 +35150,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -35579,6 +35691,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -36623,6 +36741,7 @@ "type": "boolean" }, "use_space_awareness_migration_started_at": { + "nullable": true, "type": "string" }, "use_space_awareness_migration_status": { @@ -36824,6 +36943,7 @@ "type": "boolean" }, "use_space_awareness_migration_started_at": { + "nullable": true, "type": "string" }, "use_space_awareness_migration_status": { diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 7adb2e8140491..9115670ecb313 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -6919,6 +6919,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -7943,6 +7949,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -8736,6 +8748,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -9790,6 +9808,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -10813,6 +10837,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -11607,6 +11637,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -12744,6 +12780,22 @@ ] } }, + { + "in": "query", + "name": "pkgName", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pkgVersion", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "query", "name": "previewData", @@ -30076,6 +30128,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -30563,6 +30621,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -30800,6 +30864,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -31336,6 +31406,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -32038,6 +32114,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33210,6 +33292,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33622,6 +33710,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -34314,6 +34408,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -34808,6 +34908,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -35044,6 +35150,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -35579,6 +35691,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -36623,6 +36741,7 @@ "type": "boolean" }, "use_space_awareness_migration_started_at": { + "nullable": true, "type": "string" }, "use_space_awareness_migration_status": { @@ -36824,6 +36943,7 @@ "type": "boolean" }, "use_space_awareness_migration_started_at": { + "nullable": true, "type": "string" }, "use_space_awareness_migration_status": { diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index c858823e2ae0a..68a5ccaff2e10 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -9909,6 +9909,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -10618,6 +10623,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -11167,6 +11177,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -11696,6 +11711,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -12404,6 +12424,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -12953,6 +12978,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -13891,6 +13921,16 @@ paths: type: string type: array - type: string + - in: query + name: pkgName + required: false + schema: + type: string + - in: query + name: pkgVersion + required: false + schema: + type: string - in: query name: previewData required: false @@ -25537,6 +25577,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -25865,6 +25910,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -26015,6 +26065,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -26366,6 +26421,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -26826,6 +26886,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -27325,6 +27390,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -27655,6 +27725,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -27804,6 +27879,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -28154,6 +28234,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -28931,6 +29016,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -29210,6 +29300,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -29839,6 +29934,7 @@ paths: secret_storage_requirements_met: type: boolean use_space_awareness_migration_started_at: + nullable: true type: string use_space_awareness_migration_status: enum: @@ -29972,6 +30068,7 @@ paths: secret_storage_requirements_met: type: boolean use_space_awareness_migration_started_at: + nullable: true type: string use_space_awareness_migration_status: enum: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 6780ef4926c73..208bced5d70f6 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -12769,6 +12769,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -13477,6 +13482,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -14025,6 +14035,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -14553,6 +14568,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -15260,6 +15280,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -15808,6 +15833,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -16739,6 +16769,16 @@ paths: type: string type: array - type: string + - in: query + name: pkgName + required: false + schema: + type: string + - in: query + name: pkgVersion + required: false + schema: + type: string - in: query name: previewData required: false @@ -28320,6 +28360,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -28647,6 +28692,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -28797,6 +28847,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -29148,6 +29203,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -29607,6 +29667,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -30104,6 +30169,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -30433,6 +30503,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -30582,6 +30657,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -30932,6 +31012,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -31706,6 +31791,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -31985,6 +32075,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -32607,6 +32702,7 @@ paths: secret_storage_requirements_met: type: boolean use_space_awareness_migration_started_at: + nullable: true type: string use_space_awareness_migration_status: enum: @@ -32739,6 +32835,7 @@ paths: secret_storage_requirements_met: type: boolean use_space_awareness_migration_started_at: + nullable: true type: string use_space_awareness_migration_status: enum: diff --git a/package.json b/package.json index 052daa61fb77a..5c530d4f019cf 100644 --- a/package.json +++ b/package.json @@ -479,6 +479,7 @@ "@kbn/entities-data-access-plugin": "link:x-pack/plugins/observability_solution/entities_data_access", "@kbn/entities-schema": "link:x-pack/packages/kbn-entities-schema", "@kbn/entity-manager-fixture-plugin": "link:x-pack/test/api_integration/apis/entity_manager/fixture_plugin", + "@kbn/entityManager-app-plugin": "link:x-pack/plugins/observability_solution/entity_manager_app", "@kbn/entityManager-plugin": "link:x-pack/plugins/entity_manager", "@kbn/error-boundary-example-plugin": "link:examples/error_boundary", "@kbn/es-errors": "link:packages/kbn-es-errors", @@ -802,6 +803,7 @@ "@kbn/search-index-documents": "link:packages/kbn-search-index-documents", "@kbn/search-indices": "link:x-pack/plugins/search_indices", "@kbn/search-inference-endpoints": "link:x-pack/plugins/search_inference_endpoints", + "@kbn/search-navigation": "link:x-pack/plugins/search_solution/search_navigation", "@kbn/search-notebooks": "link:x-pack/plugins/search_notebooks", "@kbn/search-playground": "link:x-pack/plugins/search_playground", "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", @@ -935,6 +937,7 @@ "@kbn/status-plugin-a-plugin": "link:test/server_integration/plugins/status_plugin_a", "@kbn/status-plugin-b-plugin": "link:test/server_integration/plugins/status_plugin_b", "@kbn/std": "link:packages/kbn-std", + "@kbn/streams-app-plugin": "link:x-pack/plugins/streams_app", "@kbn/streams-plugin": "link:x-pack/plugins/streams", "@kbn/synthetics-plugin": "link:x-pack/plugins/observability_solution/synthetics", "@kbn/synthetics-private-location": "link:x-pack/packages/kbn-synthetics-private-location", @@ -1080,7 +1083,7 @@ "ajv": "^8.12.0", "ansi-regex": "^6.1.0", "antlr4": "^4.13.1-patch-1", - "archiver": "^5.3.1", + "archiver": "^7.0.1", "async": "^3.2.3", "aws4": "^1.12.0", "axios": "^1.7.4", @@ -1219,7 +1222,7 @@ "query-string": "^6.13.2", "rbush": "^3.0.1", "re-resizable": "^6.9.9", - "re2js": "0.4.2", + "re2js": "0.4.3", "react": "^17.0.2", "react-diff-view": "^3.2.1", "react-dom": "^17.0.2", @@ -1308,7 +1311,7 @@ "zod": "^3.22.3" }, "devDependencies": { - "@apidevtools/swagger-parser": "^10.0.3", + "@apidevtools/swagger-parser": "^10.1.0", "@babel/cli": "^7.24.7", "@babel/core": "^7.24.7", "@babel/eslint-parser": "^7.24.7", @@ -1427,6 +1430,7 @@ "@kbn/core-ui-settings-server-mocks": "link:packages/core/ui-settings/core-ui-settings-server-mocks", "@kbn/core-usage-data-server-mocks": "link:packages/core/usage-data/core-usage-data-server-mocks", "@kbn/cypress-config": "link:packages/kbn-cypress-config", + "@kbn/dependency-usage": "link:packages/kbn-dependency-usage", "@kbn/dev-cli-errors": "link:packages/kbn-dev-cli-errors", "@kbn/dev-cli-runner": "link:packages/kbn-dev-cli-runner", "@kbn/dev-proc-runner": "link:packages/kbn-dev-proc-runner", @@ -1480,6 +1484,7 @@ "@kbn/repo-path": "link:packages/kbn-repo-path", "@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier", "@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli", + "@kbn/scout": "link:packages/kbn-scout", "@kbn/security-api-integration-helpers": "link:x-pack/test/security_api_integration/packages/helpers", "@kbn/serverless-storybook-config": "link:packages/serverless/storybook/config", "@kbn/some-dev-log": "link:packages/kbn-some-dev-log", @@ -1529,7 +1534,7 @@ "@storybook/testing-react": "^1.3.0", "@storybook/theming": "^6.5.16", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", @@ -1686,7 +1691,7 @@ "buildkite-test-collector": "^1.7.0", "callsites": "^3.1.0", "chance": "1.0.18", - "chromedriver": "^130.0.4", + "chromedriver": "^131.0.0", "clarify": "^2.2.0", "clean-webpack-plugin": "^3.0.0", "cli-progress": "^3.12.0", @@ -1697,7 +1702,7 @@ "cssnano": "^5.1.12", "cssnano-preset-default": "^5.2.12", "csstype": "^3.0.2", - "cypress": "13.6.3", + "cypress": "13.15.2", "cypress-axe": "^1.5.0", "cypress-file-upload": "^5.0.8", "cypress-multi-reporters": "^1.6.4", @@ -1706,6 +1711,7 @@ "cypress-recurse": "^1.35.2", "date-fns": "^2.29.3", "dependency-check": "^4.1.0", + "dependency-cruiser": "^16.4.2", "ejs": "^3.1.10", "enzyme": "^3.11.0", "enzyme-to-json": "^3.6.2", @@ -1786,7 +1792,7 @@ "null-loader": "^3.0.0", "nyc": "^15.1.0", "oboe": "^2.1.4", - "openapi-types": "^10.0.0", + "openapi-types": "^12.1.3", "p-reflect": "2.1.0", "peggy": "^1.2.0", "picomatch": "^2.3.1", diff --git a/packages/core/http/core-http-router-server-internal/src/router.test.ts b/packages/core/http/core-http-router-server-internal/src/router.test.ts index 2c702fb3ef702..f0eaa96879d42 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.test.ts @@ -305,6 +305,23 @@ describe('Router', () => { ); }); + it('throws if route has security declared wrong', () => { + const router = new Router('', logger, enhanceWithContext, routerOptions); + expect(() => + router.get( + // we use 'any' because validate requires valid Type or function usage + { + path: '/', + validate: false, + options: { security: { authz: { requiredPrivileges: [] } } } as any, + }, + (context, req, res) => res.ok({}) + ) + ).toThrowErrorMatchingInlineSnapshot( + `"\`options.security\` is not allowed in route config. Use \`security\` instead."` + ); + }); + it('throws if options.body.output is not a valid value', () => { const router = new Router('', logger, enhanceWithContext, routerOptions); expect(() => diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts index c2ea09605c4e0..c3e44f8bc9d7b 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.ts @@ -115,6 +115,11 @@ function validOptions( ); } + // @ts-expect-error to eliminate problems with `security` in the options for route factories abstractions + if (options.security) { + throw new Error('`options.security` is not allowed in route config. Use `security` instead.'); + } + const body = shouldNotHavePayload ? undefined : { diff --git a/packages/core/http/core-http-server/src/router/request.ts b/packages/core/http/core-http-server/src/router/request.ts index 066372faca1e4..6fff29eb42b54 100644 --- a/packages/core/http/core-http-server/src/router/request.ts +++ b/packages/core/http/core-http-server/src/router/request.ts @@ -48,9 +48,11 @@ export interface KibanaRequestState extends RequestApplicationState { * Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. * @public */ -export type KibanaRequestRouteOptions = Method extends 'get' | 'options' +export type KibanaRequestRouteOptions = (Method extends + | 'get' + | 'options' ? Required, 'body'>> - : Required>; + : Required>) & { security?: RouteSecurity }; /** * Request specific route information exposed to a handler. diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts index da24adcb6802e..2efd405274113 100644 --- a/packages/core/http/core-http-server/src/router/route.ts +++ b/packages/core/http/core-http-server/src/router/route.ts @@ -398,13 +398,6 @@ export interface RouteConfigOptions { */ discontinued?: string; - /** - * Defines the security requirements for a route, including authorization and authentication. - * - * @remarks This will be surfaced in OAS documentation. - */ - security?: RouteSecurity; - /** * Whether this endpoint is being used to serve generated or static HTTP resources * like JS, CSS or HTML. _Do not set to `true` for HTTP APIs._ diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index 63e1e37754803..fa08810ce1fb7 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -19,6 +19,7 @@ import type { RequestHandlerContextBase, RouteValidationFunction, LazyValidator, + RouteSecurity, } from '../..'; import type { RouteDeprecationInfo } from '../router/route'; type RqCtx = RequestHandlerContextBase; @@ -35,12 +36,12 @@ export type VersionedRouteConfig = Omit< > & { options?: Omit< RouteConfigOptions, - 'access' | 'description' | 'summary' | 'deprecated' | 'discontinued' | 'security' + 'access' | 'description' | 'summary' | 'deprecated' | 'discontinued' >; /** See {@link RouteConfigOptions['access']} */ access: Exclude['access'], undefined>; /** See {@link RouteConfigOptions['security']} */ - security?: Exclude['security'], undefined>; + security?: RouteSecurity; /** * When enabled, the router will also check for the presence of an `apiVersion` * query parameter to determine the route version to resolve to: @@ -337,7 +338,7 @@ export interface AddVersionOpts { */ validate: false | VersionedRouteValidation | (() => VersionedRouteValidation); // Provide a way to lazily load validation schemas - security?: Exclude['security'], undefined>; + security?: RouteSecurity; options?: { deprecated?: RouteDeprecationInfo; diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index 25642ba69613a..3dcc009481d0a 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -35,3 +35,5 @@ export const OBLT_UX_APP_ID = 'ux'; export const OBLT_PROFILING_APP_ID = 'profiling'; export const INVENTORY_APP_ID = 'inventory'; + +export const STREAMS_APP_ID = 'streams'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 1253b4e889fcf..256350feb2e21 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -21,6 +21,7 @@ import { OBLT_UX_APP_ID, OBLT_PROFILING_APP_ID, INVENTORY_APP_ID, + STREAMS_APP_ID, } from './constants'; type LogsApp = typeof LOGS_APP_ID; @@ -36,6 +37,7 @@ type AiAssistantApp = typeof AI_ASSISTANT_APP_ID; type ObltUxApp = typeof OBLT_UX_APP_ID; type ObltProfilingApp = typeof OBLT_PROFILING_APP_ID; type InventoryApp = typeof INVENTORY_APP_ID; +type StreamsApp = typeof STREAMS_APP_ID; export type AppId = | LogsApp @@ -50,7 +52,8 @@ export type AppId = | AiAssistantApp | ObltUxApp | ObltProfilingApp - | InventoryApp; + | InventoryApp + | StreamsApp; export type LogsLinkId = 'log-categories' | 'settings' | 'anomalies' | 'stream'; @@ -83,13 +86,16 @@ export type SyntheticsLinkId = 'certificates' | 'overview'; export type ProfilingLinkId = 'stacktraces' | 'flamegraphs' | 'functions'; +export type StreamsLinkId = 'overview'; + export type LinkId = | LogsLinkId | ObservabilityOverviewLinkId | MetricsLinkId | ApmLinkId | SyntheticsLinkId - | ProfilingLinkId; + | ProfilingLinkId + | StreamsLinkId; export type DeepLinkId = | AppId @@ -99,4 +105,5 @@ export type DeepLinkId = | `${ApmApp}:${ApmLinkId}` | `${SyntheticsApp}:${SyntheticsLinkId}` | `${ObltProfilingApp}:${ProfilingLinkId}` - | `${InventoryApp}:${InventoryLinkId}`; + | `${InventoryApp}:${InventoryLinkId}` + | `${StreamsApp}:${StreamsLinkId}`; diff --git a/packages/deeplinks/security/deep_links.ts b/packages/deeplinks/security/deep_links.ts index 644691bd5b8bc..c1d9b3b3cb6af 100644 --- a/packages/deeplinks/security/deep_links.ts +++ b/packages/deeplinks/security/deep_links.ts @@ -69,6 +69,7 @@ export enum SecurityPageName { rulesAdd = 'rules-add', rulesCreate = 'rules-create', rulesLanding = 'rules-landing', + siemMigrationsRules = 'siem_migrations-rules', /* * Warning: Computed values are not permitted in an enum with string valued members * All threat intelligence page names must match `TIPageId` in x-pack/plugins/threat_intelligence/public/common/navigation/types.ts diff --git a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/hooks/use_filters_sync_to_local_storage.test.ts b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/hooks/use_filters_sync_to_local_storage.test.ts index cb7a9b7f3c5c8..dafc4e16c899f 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/hooks/use_filters_sync_to_local_storage.test.ts +++ b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/hooks/use_filters_sync_to_local_storage.test.ts @@ -8,7 +8,7 @@ */ import type { ControlGroupRuntimeState } from '@kbn/controls-plugin/public'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, act, renderHook } from '@testing-library/react'; import { useControlGroupSyncToLocalStorage } from './use_control_group_sync_to_local_storage'; import { Storage } from '@kbn/kibana-utils-plugin/public'; @@ -34,77 +34,97 @@ describe('Filters Sync to Local Storage', () => { writable: true, }); }); + afterEach(() => { mockLocalStorage = {}; }); - it('should not be undefined if localStorage has initial value', () => { + + it('should not be undefined if localStorage has initial value', async () => { global.localStorage.setItem(TEST_STORAGE_KEY, JSON.stringify(DEFAULT_STORED_VALUE)); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useControlGroupSyncToLocalStorage({ Storage, storageKey: TEST_STORAGE_KEY, shouldSync: true, }) ); - waitForNextUpdate(); - expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE); + await waitFor(() => + expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE) + ); }); - it('should be undefined if localstorage as NO initial value', () => { - const { result, waitForNextUpdate } = renderHook(() => + + it('should be undefined if localstorage as NO initial value', async () => { + const { result } = renderHook(() => useControlGroupSyncToLocalStorage({ Storage, storageKey: TEST_STORAGE_KEY, shouldSync: true, }) ); - waitForNextUpdate(); - expect(result.current.controlGroupState).toBeUndefined(); - expect(result.current.setControlGroupState).toBeTruthy(); + await waitFor(() => + expect(result.current).toEqual( + expect.objectContaining({ + controlGroupState: undefined, + setControlGroupState: expect.any(Function), + }) + ) + ); }); - it('should be update values to local storage when sync is ON', () => { - const { result, waitFor } = renderHook(() => + it('should be update values to local storage when sync is ON', async () => { + const { result } = renderHook(() => useControlGroupSyncToLocalStorage({ Storage, storageKey: TEST_STORAGE_KEY, shouldSync: true, }) ); - waitFor(() => { + await waitFor(() => { expect(result.current.controlGroupState).toBeUndefined(); expect(result.current.setControlGroupState).toBeTruthy(); }); - result.current.setControlGroupState(DEFAULT_STORED_VALUE); - waitFor(() => { + act(() => { + result.current.setControlGroupState(DEFAULT_STORED_VALUE); + }); + await waitFor(() => { expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE); expect(global.localStorage.getItem(TEST_STORAGE_KEY)).toBe( JSON.stringify(DEFAULT_STORED_VALUE) ); }); }); - it('should not update values to local storage when sync is OFF', () => { - const { waitFor, result, rerender } = renderHook(() => - useControlGroupSyncToLocalStorage({ - Storage, - storageKey: TEST_STORAGE_KEY, - shouldSync: true, - }) - ); + it('should not update values to local storage when sync is OFF', async () => { + const initialProps = { + Storage, + storageKey: TEST_STORAGE_KEY, + shouldSync: true, + }; + + const { result, rerender } = renderHook(useControlGroupSyncToLocalStorage, { + initialProps, + }); // Sync is ON - waitFor(() => { + await waitFor(() => { expect(result.current.controlGroupState).toBeUndefined(); expect(result.current.setControlGroupState).toBeTruthy(); }); - result.current.setControlGroupState(DEFAULT_STORED_VALUE); - waitFor(() => { + act(() => { + result.current.setControlGroupState(DEFAULT_STORED_VALUE); + }); + + await waitFor(() => { expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE); }); // Sync is OFF - rerender({ storageKey: TEST_STORAGE_KEY, shouldSync: false }); - result.current.setControlGroupState(ANOTHER_SAMPLE_VALUE); - waitFor(() => { + rerender({ ...initialProps, shouldSync: false }); + + act(() => { + result.current.setControlGroupState(ANOTHER_SAMPLE_VALUE); + }); + + await waitFor(() => { expect(result.current.controlGroupState).toMatchObject(ANOTHER_SAMPLE_VALUE); // old value expect(global.localStorage.getItem(TEST_STORAGE_KEY)).toBe( diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_alerts_data_view.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_alerts_data_view.test.tsx index 2f4e8598a4cf6..b4945f298a696 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_alerts_data_view.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_alerts_data_view.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { renderHook, waitFor } from '@testing-library/react'; import { DataView } from '@kbn/data-views-plugin/common'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; @@ -69,7 +69,7 @@ describe('useAlertsDataView', () => { dataView: undefined, }; - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAlertsDataView({ ...mockServices, @@ -84,7 +84,7 @@ describe('useAlertsDataView', () => { }); it('fetches indexes and fields for non-siem feature ids, returning a DataViewBase object', async () => { - const { result, waitForValueToChange } = renderHook( + const { result } = renderHook( () => useAlertsDataView({ ...mockServices, @@ -95,7 +95,7 @@ describe('useAlertsDataView', () => { } ); - await waitForValueToChange(() => result.current.isLoading, { timeout: 5000 }); + await waitFor(() => result.current.isLoading, { timeout: 5000 }); expect(mockFetchAlertsFields).toHaveBeenCalledTimes(1); expect(mockFetchAlertsIndexNames).toHaveBeenCalledTimes(1); @@ -103,7 +103,7 @@ describe('useAlertsDataView', () => { }); it('only fetches index names for the siem feature id, returning a DataView', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAlertsDataView({ ...mockServices, featureIds: [AlertConsumers.SIEM] }), { wrapper, @@ -117,7 +117,7 @@ describe('useAlertsDataView', () => { }); it('does not fetch anything if siem and other feature ids are mixed together', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAlertsDataView({ ...mockServices, @@ -141,7 +141,7 @@ describe('useAlertsDataView', () => { it('returns an undefined data view if any of the queries fails', async () => { mockFetchAlertsIndexNames.mockRejectedValue('error'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAlertsDataView({ ...mockServices, featureIds: observabilityFeatureIds }), { wrapper, @@ -159,12 +159,9 @@ describe('useAlertsDataView', () => { it('shows an error toast if any of the queries fails', async () => { mockFetchAlertsIndexNames.mockRejectedValue('error'); - const { waitFor } = renderHook( - () => useAlertsDataView({ ...mockServices, featureIds: observabilityFeatureIds }), - { - wrapper, - } - ); + renderHook(() => useAlertsDataView({ ...mockServices, featureIds: observabilityFeatureIds }), { + wrapper, + }); await waitFor(() => expect(mockServices.toasts.addDanger).toHaveBeenCalled()); }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.test.tsx index b51f878592da8..46768f355dbcf 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { HttpStart } from '@kbn/core-http-browser'; import { useCreateRule } from './use_create_rule'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_fields_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_fields_query.test.tsx index 3607e75bc868e..39818fab28cbf 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_fields_query.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_fields_query.test.tsx @@ -10,7 +10,7 @@ import React, { FC } from 'react'; import { AlertConsumers } from '@kbn/rule-data-utils'; import * as ReactQuery from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { testQueryClientConfig } from '../test_utils/test_query_client_config'; import { useFetchAlertsFieldsQuery } from './use_fetch_alerts_fields_query'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; @@ -88,35 +88,37 @@ describe('useFetchAlertsFieldsQuery', () => { }); it('should call the api only once', async () => { - const { result, rerender, waitForValueToChange } = renderHook( + const { result, rerender } = renderHook( () => useFetchAlertsFieldsQuery({ http: mockHttpClient, featureIds: ['apm'] }), { wrapper, } ); - await waitForValueToChange(() => result.current.data); - - expect(mockHttpGet).toHaveBeenCalledTimes(1); - expect(result.current.data).toEqual({ - browserFields: { fakeCategory: {} }, - fields: [ - { - name: 'fakeCategory', - }, - ], + await waitFor(() => { + expect(mockHttpGet).toHaveBeenCalledTimes(1); + expect(result.current.data).toEqual({ + browserFields: { fakeCategory: {} }, + fields: [ + { + name: 'fakeCategory', + }, + ], + }); }); rerender(); - expect(mockHttpGet).toHaveBeenCalledTimes(1); - expect(result.current.data).toEqual({ - browserFields: { fakeCategory: {} }, - fields: [ - { - name: 'fakeCategory', - }, - ], + await waitFor(() => { + expect(mockHttpGet).toHaveBeenCalledTimes(1); + expect(result.current.data).toEqual({ + browserFields: { fakeCategory: {} }, + fields: [ + { + name: 'fakeCategory', + }, + ], + }); }); }); @@ -132,8 +134,10 @@ describe('useFetchAlertsFieldsQuery', () => { } ); - expect(mockHttpGet).toHaveBeenCalledTimes(0); - expect(result.current.data).toEqual(emptyData); + await waitFor(() => { + expect(mockHttpGet).toHaveBeenCalledTimes(0); + expect(result.current.data).toEqual(emptyData); + }); }); it('should not fetch if all featureId are not valid', async () => { @@ -148,8 +152,10 @@ describe('useFetchAlertsFieldsQuery', () => { } ); - expect(mockHttpGet).toHaveBeenCalledTimes(0); - expect(result.current.data).toEqual(emptyData); + await waitFor(() => { + expect(mockHttpGet).toHaveBeenCalledTimes(0); + expect(result.current.data).toEqual(emptyData); + }); }); it('should filter out the non valid feature id', async () => { @@ -164,9 +170,11 @@ describe('useFetchAlertsFieldsQuery', () => { } ); - expect(mockHttpGet).toHaveBeenCalledTimes(1); - expect(mockHttpGet).toHaveBeenCalledWith('/internal/rac/alerts/browser_fields', { - query: { featureIds: ['apm', 'logs'] }, + await waitFor(() => { + expect(mockHttpGet).toHaveBeenCalledTimes(1); + expect(mockHttpGet).toHaveBeenCalledWith('/internal/rac/alerts/browser_fields', { + query: { featureIds: ['apm', 'logs'] }, + }); }); }); }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_index_names_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_index_names_query.test.tsx index ab702a2ea09ec..c3480a6ce2675 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_index_names_query.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_alerts_index_names_query.test.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { testQueryClientConfig } from '../test_utils/test_query_client_config'; import { useFetchAlertsIndexNamesQuery } from './use_fetch_alerts_index_names_query'; import { fetchAlertsIndexNames } from '../apis/fetch_alerts_index_names'; @@ -56,14 +56,14 @@ describe('useFetchAlertsIndexNamesQuery', () => { }); it('correctly caches the index names', async () => { - const { result, rerender, waitForValueToChange } = renderHook( + const { result, rerender } = renderHook( () => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, featureIds: ['apm'] }), { wrapper, } ); - await waitForValueToChange(() => result.current.data); + await waitFor(() => result.current.data); expect(mockFetchAlertsIndexNames).toHaveBeenCalledTimes(1); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx index 10e1869b9e64c..6aad133fee5e6 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { testQueryClientConfig } from '../test_utils/test_query_client_config'; import { useFetchFlappingSettings } from './use_fetch_flapping_settings'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; @@ -43,12 +43,9 @@ describe('useFetchFlappingSettings', () => { }); test('should call fetchFlappingSettings with the correct parameters', async () => { - const { result, waitFor } = renderHook( - () => useFetchFlappingSettings({ http, enabled: true }), - { - wrapper, - } - ); + const { result } = renderHook(() => useFetchFlappingSettings({ http, enabled: true }), { + wrapper, + }); await waitFor(() => { return expect(result.current.isInitialLoading).toEqual(false); @@ -66,12 +63,9 @@ describe('useFetchFlappingSettings', () => { }); test('should not call fetchFlappingSettings if enabled is false', async () => { - const { result, waitFor } = renderHook( - () => useFetchFlappingSettings({ http, enabled: false }), - { - wrapper, - } - ); + const { result } = renderHook(() => useFetchFlappingSettings({ http, enabled: false }), { + wrapper, + }); await waitFor(() => { return expect(result.current.isInitialLoading).toEqual(false); @@ -82,7 +76,7 @@ describe('useFetchFlappingSettings', () => { test('should call onSuccess when the fetching was successful', async () => { const onSuccessMock = jest.fn(); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useFetchFlappingSettings({ http, enabled: true, onSuccess: onSuccessMock }), { wrapper, diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.test.tsx index 14aca036ed87e..50dcf3d7e2900 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.test.tsx @@ -9,13 +9,12 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; import type { HttpStart } from '@kbn/core-http-browser'; import { ToastsStart } from '@kbn/core-notifications-browser'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { useGetAlertsGroupAggregationsQuery } from './use_get_alerts_group_aggregations_query'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { BASE_RAC_ALERTS_API_PATH } from '../constants'; const queryClient = new QueryClient({ diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_health_check.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_health_check.test.tsx index da4d5bc3a878c..ecf4ddc685e27 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_health_check.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_health_check.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { HttpStart } from '@kbn/core-http-browser'; import { useHealthCheck } from './use_health_check'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connector_types.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connector_types.test.tsx index 63e494ab87084..cc21ecef97458 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connector_types.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connector_types.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { useLoadConnectorTypes } from './use_load_connector_types'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.test.tsx index 6ab5c58cb1514..7d81a67ebce94 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { useLoadConnectors } from './use_load_connectors'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.test.tsx index 7d3c0815ffae5..0af35ccddba4e 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { useLoadRuleTypeAadTemplateField } from './use_load_rule_type_aad_template_fields'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.test.tsx index 4f74179fa8d52..69c3234d812f0 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { HttpStart } from '@kbn/core-http-browser'; import { useResolveRule } from './use_resolve_rule'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx index 664a525796d42..4d1d8c93407e7 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx @@ -12,7 +12,7 @@ import { of } from 'rxjs'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { IKibanaSearchResponse } from '@kbn/search-types'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import type { UseSearchAlertsQueryParams } from './use_search_alerts_query'; import { AlertsQueryContext } from '../contexts/alerts_query_context'; import { useSearchAlertsQuery } from './use_search_alerts_query'; @@ -126,126 +126,128 @@ describe('useSearchAlertsQuery', () => { }); it('returns the response correctly', async () => { - const { result, waitForValueToChange } = renderHook(() => useSearchAlertsQuery(params), { + const { result } = renderHook(() => useSearchAlertsQuery(params), { wrapper, }); - await waitForValueToChange(() => result.current.data); - expect(result.current.data).toEqual( - expect.objectContaining({ - ...expectedResponse, - alerts: [ - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', - '@timestamp': ['2022-03-22T16:48:07.518Z'], - 'host.name': ['Host-4dbzugdlqd'], - 'kibana.alert.reason': [ - 'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.', - ], - 'kibana.alert.risk_score': [21], - 'kibana.alert.rule.name': ['test'], - 'kibana.alert.severity': ['low'], - 'process.name': ['iexlorer.exe'], - 'user.name': ['5qcxz8o4j7'], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', - '@timestamp': ['2022-03-22T16:17:50.769Z'], - 'host.name': ['Host-4dbzugdlqd'], - 'kibana.alert.reason': [ - 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', - ], - 'kibana.alert.risk_score': [21], - 'kibana.alert.rule.name': ['test'], - 'kibana.alert.severity': ['low'], - 'process.name': ['iexlorer.exe'], - 'user.name': ['hdgsmwj08h'], - }, - ], - total: 2, - ecsAlertsData: [ - { - kibana: { - alert: { - severity: ['low'], - risk_score: [21], - rule: { name: ['test'] }, - reason: [ - 'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.', - ], - }, - }, - process: { name: ['iexlorer.exe'] }, - '@timestamp': ['2022-03-22T16:48:07.518Z'], - user: { name: ['5qcxz8o4j7'] }, - host: { name: ['Host-4dbzugdlqd'] }, - _id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', - _index: '.internal.alerts-security.alerts-default-000001', - }, - { - kibana: { - alert: { - severity: ['low'], - risk_score: [21], - rule: { name: ['test'] }, - reason: [ - 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', - ], - }, - }, - process: { name: ['iexlorer.exe'] }, - '@timestamp': ['2022-03-22T16:17:50.769Z'], - user: { name: ['hdgsmwj08h'] }, - host: { name: ['Host-4dbzugdlqd'] }, - _id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', - _index: '.internal.alerts-security.alerts-default-000001', - }, - ], - oldAlertsData: [ - [ - { field: 'kibana.alert.severity', value: ['low'] }, - { field: 'process.name', value: ['iexlorer.exe'] }, - { field: '@timestamp', value: ['2022-03-22T16:48:07.518Z'] }, - { field: 'kibana.alert.risk_score', value: [21] }, - { field: 'kibana.alert.rule.name', value: ['test'] }, - { field: 'user.name', value: ['5qcxz8o4j7'] }, + await waitFor(() => { + expect(result.current.data).toBeDefined(); + expect(result.current.data).toEqual( + expect.objectContaining({ + ...expectedResponse, + alerts: [ { - field: 'kibana.alert.reason', - value: [ + _index: '.internal.alerts-security.alerts-default-000001', + _id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', + '@timestamp': ['2022-03-22T16:48:07.518Z'], + 'host.name': ['Host-4dbzugdlqd'], + 'kibana.alert.reason': [ 'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.', ], + 'kibana.alert.risk_score': [21], + 'kibana.alert.rule.name': ['test'], + 'kibana.alert.severity': ['low'], + 'process.name': ['iexlorer.exe'], + 'user.name': ['5qcxz8o4j7'], }, - { field: 'host.name', value: ['Host-4dbzugdlqd'] }, { - field: '_id', - value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', + _index: '.internal.alerts-security.alerts-default-000001', + _id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', + '@timestamp': ['2022-03-22T16:17:50.769Z'], + 'host.name': ['Host-4dbzugdlqd'], + 'kibana.alert.reason': [ + 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', + ], + 'kibana.alert.risk_score': [21], + 'kibana.alert.rule.name': ['test'], + 'kibana.alert.severity': ['low'], + 'process.name': ['iexlorer.exe'], + 'user.name': ['hdgsmwj08h'], }, - { field: '_index', value: '.internal.alerts-security.alerts-default-000001' }, ], - [ - { field: 'kibana.alert.severity', value: ['low'] }, - { field: 'process.name', value: ['iexlorer.exe'] }, - { field: '@timestamp', value: ['2022-03-22T16:17:50.769Z'] }, - { field: 'kibana.alert.risk_score', value: [21] }, - { field: 'kibana.alert.rule.name', value: ['test'] }, - { field: 'user.name', value: ['hdgsmwj08h'] }, + total: 2, + ecsAlertsData: [ { - field: 'kibana.alert.reason', - value: [ - 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', - ], + kibana: { + alert: { + severity: ['low'], + risk_score: [21], + rule: { name: ['test'] }, + reason: [ + 'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.', + ], + }, + }, + process: { name: ['iexlorer.exe'] }, + '@timestamp': ['2022-03-22T16:48:07.518Z'], + user: { name: ['5qcxz8o4j7'] }, + host: { name: ['Host-4dbzugdlqd'] }, + _id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', + _index: '.internal.alerts-security.alerts-default-000001', }, - { field: 'host.name', value: ['Host-4dbzugdlqd'] }, { - field: '_id', - value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', + kibana: { + alert: { + severity: ['low'], + risk_score: [21], + rule: { name: ['test'] }, + reason: [ + 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', + ], + }, + }, + process: { name: ['iexlorer.exe'] }, + '@timestamp': ['2022-03-22T16:17:50.769Z'], + user: { name: ['hdgsmwj08h'] }, + host: { name: ['Host-4dbzugdlqd'] }, + _id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', + _index: '.internal.alerts-security.alerts-default-000001', }, - { field: '_index', value: '.internal.alerts-security.alerts-default-000001' }, ], - ], - }) - ); + oldAlertsData: [ + [ + { field: 'kibana.alert.severity', value: ['low'] }, + { field: 'process.name', value: ['iexlorer.exe'] }, + { field: '@timestamp', value: ['2022-03-22T16:48:07.518Z'] }, + { field: 'kibana.alert.risk_score', value: [21] }, + { field: 'kibana.alert.rule.name', value: ['test'] }, + { field: 'user.name', value: ['5qcxz8o4j7'] }, + { + field: 'kibana.alert.reason', + value: [ + 'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.', + ], + }, + { field: 'host.name', value: ['Host-4dbzugdlqd'] }, + { + field: '_id', + value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1', + }, + { field: '_index', value: '.internal.alerts-security.alerts-default-000001' }, + ], + [ + { field: 'kibana.alert.severity', value: ['low'] }, + { field: 'process.name', value: ['iexlorer.exe'] }, + { field: '@timestamp', value: ['2022-03-22T16:17:50.769Z'] }, + { field: 'kibana.alert.risk_score', value: [21] }, + { field: 'kibana.alert.rule.name', value: ['test'] }, + { field: 'user.name', value: ['hdgsmwj08h'] }, + { + field: 'kibana.alert.reason', + value: [ + 'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.', + ], + }, + { field: 'host.name', value: ['Host-4dbzugdlqd'] }, + { + field: '_id', + value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86', + }, + { field: '_index', value: '.internal.alerts-security.alerts-default-000001' }, + ], + ], + }) + ); + }); }); it('returns empty placeholder data', () => { diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.test.tsx index ec3579f20db51..654166f4bbac9 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { HttpStart } from '@kbn/core-http-browser'; import { useUpdateRule } from './use_update_rule'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_virtual_data_view_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_virtual_data_view_query.test.tsx index 834409a87f52a..09be28ec15034 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_virtual_data_view_query.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_virtual_data_view_query.test.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import * as ReactQuery from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { testQueryClientConfig } from '../test_utils/test_query_client_config'; import { queryKeyPrefix, useVirtualDataViewQuery } from './use_virtual_data_view_query'; import { DataView } from '@kbn/data-views-plugin/common'; @@ -38,10 +38,11 @@ describe('useVirtualDataViewQuery', () => { it('does not create a data view if indexNames is empty or nullish', () => { const { rerender } = renderHook( - ({ indexNames }: React.PropsWithChildren<{ indexNames: string[] }>) => + ({ indexNames }: { indexNames?: string[] }) => useVirtualDataViewQuery({ dataViewsService: mockDataViewsService, indexNames }), { wrapper, + initialProps: {}, } ); @@ -89,7 +90,7 @@ describe('useVirtualDataViewQuery', () => { }); it('removes the data view from the instance cache on unmount', async () => { - const { result, waitForValueToChange, unmount } = renderHook( + const { result, unmount } = renderHook( () => useVirtualDataViewQuery({ dataViewsService: mockDataViewsService, @@ -100,10 +101,10 @@ describe('useVirtualDataViewQuery', () => { } ); - await waitForValueToChange(() => result.current.data); + await waitFor(() => expect(result.current.data).toBeDefined()); unmount(); - expect(mockDataViewsService.clearInstanceCache).toHaveBeenCalled(); + await waitFor(() => expect(mockDataViewsService.clearInstanceCache).toHaveBeenCalled()); }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx index f0a14ac82e4a6..3dd6a6a3cee11 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { HttpStart } from '@kbn/core-http-browser'; import type { ToastsStart } from '@kbn/core-notifications-browser'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.test.tsx index a64dcca57387f..c94062e320a8d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.test.tsx @@ -21,6 +21,7 @@ import { import { ActionTypeModel } from '../../common'; import { RuleActionsMessageProps } from './rule_actions_message'; import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item'; +import { I18nProvider } from '@kbn/i18n-react'; jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), @@ -81,7 +82,7 @@ const { validateParamsForWarnings } = jest.requireMock( '../validation/validate_params_for_warnings' ); -const mockConnectors = [getConnector('1', { id: 'action-1' })]; +const mockConnectors = [getConnector('1', { id: 'action-1', isSystemAction: true })]; const mockActionTypes = [getActionType('1')]; @@ -260,4 +261,59 @@ describe('ruleActionsSystemActionsItem', () => { expect(screen.getByText('warning message!')).toBeInTheDocument(); }); + + describe('licensing', () => { + it('should render the licensing message if the user does not have the sufficient license', async () => { + const mockConnectorsWithLicensing = [ + getConnector('1', { id: 'action-1', isSystemAction: true }), + ]; + const mockActionTypesWithLicensing = [ + getActionType('1', { + enabledInLicense: false, + minimumLicenseRequired: 'platinum' as const, + }), + ]; + + const actionTypeRegistry = new TypeRegistry(); + actionTypeRegistry.register( + getActionTypeModel('1', { + id: 'actionType-1', + validateParams: mockValidate, + }) + ); + useRuleFormState.mockReturnValue({ + plugins: { + actionTypeRegistry, + http: { + basePath: { + publicBaseUrl: 'publicUrl', + }, + }, + }, + actionsParamsErrors: {}, + selectedRuleType: { + ...ruleType, + enabledInLicense: false, + minimumLicenseRequired: 'platinum' as const, + }, + aadTemplateFields: [], + connectors: mockConnectorsWithLicensing, + connectorTypes: mockActionTypesWithLicensing, + }); + + render( + + + + ); + + expect( + await screen.findByText('This feature requires a Platinum license.') + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.tsx index 4598d42d91aac..7432aa4c4343d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_system_actions_item.tsx @@ -28,7 +28,7 @@ import { RuleActionParam, RuleSystemAction } from '@kbn/alerting-types'; import { SavedObjectAttribute } from '@kbn/core/types'; import { css } from '@emotion/react'; import { useRuleFormDispatch, useRuleFormState } from '../hooks'; -import { RuleFormParamsErrors } from '../../common'; +import { ActionConnector, RuleFormParamsErrors } from '../../common'; import { ACTION_ERROR_TOOLTIP, ACTION_WARNING_TITLE, @@ -38,6 +38,11 @@ import { import { RuleActionsMessage } from './rule_actions_message'; import { validateParamsForWarnings } from '../validation'; import { getAvailableActionVariables } from '../../action_variables'; +import { + IsDisabledResult, + IsEnabledResult, + checkActionFormActionTypeEnabled, +} from '../utils/check_action_type_enabled'; interface RuleActionsSystemActionsItemProps { action: RuleSystemAction; @@ -45,6 +50,64 @@ interface RuleActionsSystemActionsItemProps { producerId: string; } +interface SystemActionAccordionContentProps extends RuleActionsSystemActionsItemProps { + connector: ActionConnector; + checkEnabledResult?: IsEnabledResult | IsDisabledResult | null; + warning?: string | null; + onParamsChange: (key: string, value: RuleActionParam) => void; +} + +const SystemActionAccordionContent: React.FC = React.memo( + ({ connector, checkEnabledResult, action, index, producerId, warning, onParamsChange }) => { + const { aadTemplateFields } = useRuleFormState(); + const { euiTheme } = useEuiTheme(); + const plain = useEuiBackgroundColor('plain'); + + if (!connector || !checkEnabledResult) { + return null; + } + + if (!checkEnabledResult.isEnabled) { + return ( + + {checkEnabledResult.messageCard} + + ); + } + + return ( + + + + + + ); + } +); + export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItemProps) => { const { action, index, producerId } = props; @@ -54,7 +117,6 @@ export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItem selectedRuleType, connectorTypes, connectors, - aadTemplateFields, } = useRuleFormState(); const [isOpen, setIsOpen] = useState(true); @@ -64,7 +126,6 @@ export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItem const [warning, setWarning] = useState(null); const subdued = useEuiBackgroundColor('subdued'); - const plain = useEuiBackgroundColor('plain'); const { euiTheme } = useEuiTheme(); const dispatch = useRuleFormDispatch(); @@ -156,6 +217,13 @@ export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItem ] ); + const checkEnabledResult = useMemo(() => { + if (!actionType) { + return null; + } + return checkActionFormActionTypeEnabled(actionType, []); + }, [actionType]); + return ( } > - - - - - + ); }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx index d8e6380462f9b..f560c3ace22ad 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx @@ -8,7 +8,7 @@ */ import React, { useReducer } from 'react'; -import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { renderHook, act } from '@testing-library/react'; import { ruleFormStateReducer } from './rule_form_state_reducer'; import { RuleFormState } from '../types'; import { getAction } from '../../common/test_utils/actions_test_utils'; diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/index.ts index 8665ef0120e25..4b316ebb822e5 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/index.ts @@ -15,15 +15,16 @@ import { k8sClusterJobEntity } from './kubernetes/cluster_entity'; import { k8sCronJobEntity } from './kubernetes/cron_job_entity'; import { k8sDaemonSetEntity } from './kubernetes/daemon_set_entity'; import { k8sDeploymentEntity } from './kubernetes/deployment_entity'; -import { k8sJobSetEntity } from './kubernetes/job_set_entity'; +import { k8sJobEntity } from './kubernetes/job_entity'; import { k8sNodeEntity } from './kubernetes/node_entity'; import { k8sPodEntity } from './kubernetes/pod_entity'; import { k8sReplicaSetEntity } from './kubernetes/replica_set'; import { k8sStatefulSetEntity } from './kubernetes/stateful_set'; +import { k8sServiceEntity } from './kubernetes/service'; import { k8sContainerEntity } from './kubernetes/container_entity'; export type EntityDataStreamType = 'metrics' | 'logs' | 'traces'; -export type Schema = 'ecs' | 'semconv'; +export type Schema = 'ecs' | 'otel'; export type EntityFields = Fields & Partial<{ @@ -52,11 +53,12 @@ export const entities = { k8sCronJobEntity, k8sDaemonSetEntity, k8sDeploymentEntity, - k8sJobSetEntity, + k8sJobEntity, k8sNodeEntity, k8sPodEntity, k8sReplicaSetEntity, k8sStatefulSetEntity, + k8sServiceEntity, k8sContainerEntity, }, }; diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cluster_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cluster_entity.ts index 9fa4c81d86ffb..487ddc89a8c6f 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cluster_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cluster_entity.ts @@ -23,6 +23,7 @@ export function k8sClusterJobEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'cluster', 'entity.type': 'cluster', 'orchestrator.cluster.name': name, 'entity.id': entityId, @@ -31,6 +32,7 @@ export function k8sClusterJobEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'cluster', 'entity.type': 'cluster', 'k8s.cluster.uid': name, 'entity.id': entityId, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/container_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/container_entity.ts index b05d412b0dd5c..116563e731f0c 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/container_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/container_entity.ts @@ -23,6 +23,7 @@ export function k8sContainerEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'container', 'entity.type': 'container', 'kubernetes.container.id': id, 'entity.id': entityId, @@ -31,6 +32,7 @@ export function k8sContainerEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'container', 'entity.type': 'container', 'container.id': id, 'entity.id': entityId, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cron_job_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cron_job_entity.ts index 8590378e699fb..86e17b5d4cfc5 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cron_job_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/cron_job_entity.ts @@ -27,9 +27,9 @@ export function k8sCronJobEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { - 'entity.type': 'cron_job', + 'entity.definition_id': 'cron_job', + 'entity.type': 'cronjob', 'kubernetes.cronjob.name': name, - 'kubernetes.cronjob.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,7 +37,8 @@ export function k8sCronJobEntity({ } return new K8sEntity(schema, { - 'entity.type': 'cron_job', + 'entity.definition_id': 'cron_job', + 'entity.type': 'cronjob', 'k8s.cronjob.name': name, 'k8s.cronjob.uid': uid, 'k8s.cluster.name': clusterName, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/daemon_set_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/daemon_set_entity.ts index 7e20b1c6d506f..59fe25dedf5a5 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/daemon_set_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/daemon_set_entity.ts @@ -27,7 +27,8 @@ export function k8sDaemonSetEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { - 'entity.type': 'daemon_set', + 'entity.definition_id': 'daemon_set', + 'entity.type': 'daemonset', 'kubernetes.daemonset.name': name, 'kubernetes.daemonset.uid': uid, 'kubernetes.namespace': clusterName, @@ -37,7 +38,8 @@ export function k8sDaemonSetEntity({ } return new K8sEntity(schema, { - 'entity.type': 'daemon_set', + 'entity.definition_id': 'daemon_set', + 'entity.type': 'daemonset', 'k8s.daemonset.name': name, 'k8s.daemonset.uid': uid, 'k8s.cluster.name': clusterName, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/deployment_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/deployment_entity.ts index 7eabdd0827325..bc266d554f6df 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/deployment_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/deployment_entity.ts @@ -27,9 +27,9 @@ export function k8sDeploymentEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'deployment', 'entity.type': 'deployment', 'kubernetes.deployment.name': name, - 'kubernetes.deployment.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,6 +37,7 @@ export function k8sDeploymentEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'deployment', 'entity.type': 'deployment', 'k8s.deployment.name': name, 'k8s.deployment.uid': uid, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts index 6da1decaab9ab..ba6cb1f892f47 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts @@ -12,27 +12,28 @@ import { Serializable } from '../../serializable'; const identityFieldsMap: Record> = { ecs: { - pod: ['kubernetes.pod.name'], + pod: ['kubernetes.pod.uid'], cluster: ['orchestrator.cluster.name'], - cron_job: ['kubernetes.cronjob.name'], - daemon_set: ['kubernetes.daemonset.name'], + cronjob: ['kubernetes.cronjob.name'], + daemonset: ['kubernetes.daemonset.name'], deployment: ['kubernetes.deployment.name'], job: ['kubernetes.job.name'], node: ['kubernetes.node.name'], - replica_set: ['kubernetes.replicaset.name'], - stateful_set: ['kubernetes.statefulset.name'], + replicaset: ['kubernetes.replicaset.name'], + statefulset: ['kubernetes.statefulset.name'], + service: ['kubernetes.service.name'], container: ['kubernetes.container.id'], }, - semconv: { - pod: ['k8s.pod.name'], + otel: { + pod: ['k8s.pod.uid'], cluster: ['k8s.cluster.uid'], - cron_job: ['k8s.cronjob.name'], - daemon_set: ['k8s.daemonset.name'], - deployment: ['k8s.deployment.name'], - job: ['k8s.job.name'], + cronjob: ['k8s.cronjob.uid'], + daemonset: ['k8s.daemonset.uid'], + deployment: ['k8s.deployment.uid'], + job: ['k8s.job.uid'], node: ['k8s.node.uid'], - replica_set: ['k8s.replicaset.name'], - stateful_set: ['k8s.statefulset.name'], + replicaset: ['k8s.replicaset.uid'], + statefulset: ['k8s.statefulset.uid'], container: ['container.id'], }, }; @@ -41,10 +42,17 @@ export class K8sEntity extends Serializable { constructor(schema: Schema, fields: EntityFields) { const entityType = fields['entity.type']; if (entityType === undefined) { - throw new Error(`Entity type not defined: ${entityType}`); + throw new Error(`Entity type not defined`); } - const entityTypeWithSchema = `kubernetes_${entityType}_${schema}`; + const entityDefinitionId = fields['entity.definition_id']; + if (entityDefinitionId === undefined) { + throw new Error(`Entity definition id not defined`); + } + + const entityDefinitionWithSchema = `kubernetes_${entityDefinitionId}_${ + schema === 'ecs' ? schema : 'semconv' + }`; const identityFields = identityFieldsMap[schema][entityType]; if (identityFields === undefined || identityFields.length === 0) { throw new Error( @@ -54,8 +62,8 @@ export class K8sEntity extends Serializable { super({ ...fields, - 'entity.type': entityTypeWithSchema, - 'entity.definition_id': `builtin_${entityTypeWithSchema}`, + 'entity.type': `k8s.${entityType}.${schema}`, + 'entity.definition_id': `builtin_${entityDefinitionWithSchema}`, 'entity.identity_fields': identityFields, 'entity.display_name': getDisplayName({ identityFields, fields }), 'entity.definition_version': '1.0.0', diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_set_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_entity.ts similarity index 91% rename from packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_set_entity.ts rename to packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_entity.ts index e0383563c7266..007f74d8a5bba 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_set_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/job_entity.ts @@ -10,7 +10,7 @@ import { Schema } from '..'; import { K8sEntity } from '.'; -export function k8sJobSetEntity({ +export function k8sJobEntity({ schema, name, uid, @@ -27,9 +27,9 @@ export function k8sJobSetEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'job', 'entity.type': 'job', 'kubernetes.job.name': name, - 'kubernetes.job.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,6 +37,7 @@ export function k8sJobSetEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'job', 'entity.type': 'job', 'k8s.job.name': name, 'k8s.job.uid': uid, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/node_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/node_entity.ts index 283df5250d41d..4ab3441d7b82b 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/node_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/node_entity.ts @@ -27,9 +27,9 @@ export function k8sNodeEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'node', 'entity.type': 'node', 'kubernetes.node.name': name, - 'kubernetes.node.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,6 +37,7 @@ export function k8sNodeEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'node', 'entity.type': 'node', 'k8s.node.uid': uid, 'k8s.cluster.name': clusterName, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/pod_entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/pod_entity.ts index 1b71c4e39a4fc..47c24b01144f1 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/pod_entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/pod_entity.ts @@ -27,6 +27,7 @@ export function k8sPodEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { + 'entity.definition_id': 'pod', 'entity.type': 'pod', 'kubernetes.pod.name': name, 'kubernetes.pod.uid': uid, @@ -37,6 +38,7 @@ export function k8sPodEntity({ } return new K8sEntity(schema, { + 'entity.definition_id': 'pod', 'entity.type': 'pod', 'k8s.pod.name': name, 'k8s.pod.uid': uid, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/replica_set.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/replica_set.ts index fcf20c0530c30..b98397d2bee9b 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/replica_set.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/replica_set.ts @@ -27,9 +27,9 @@ export function k8sReplicaSetEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { - 'entity.type': 'replica_set', + 'entity.definition_id': 'replica_set', + 'entity.type': 'replicaset', 'kubernetes.replicaset.name': name, - 'kubernetes.replicaset.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,7 +37,8 @@ export function k8sReplicaSetEntity({ } return new K8sEntity(schema, { - 'entity.type': 'replica_set', + 'entity.definition_id': 'replica_set', + 'entity.type': 'replicaset', 'k8s.replicaset.name': name, 'k8s.replicaset.uid': uid, 'k8s.cluster.name': clusterName, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/service.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/service.ts new file mode 100644 index 0000000000000..4793048aeee02 --- /dev/null +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/service.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Schema } from '..'; +import { K8sEntity } from '.'; + +export function k8sServiceEntity({ + schema, + name, + uid, + clusterName, + entityId, + ...others +}: { + schema: Schema; + name: string; + uid?: string; + clusterName?: string; + entityId: string; + [key: string]: any; +}) { + if (schema !== 'ecs') { + throw new Error('Schema not supported for service entity: ' + schema); + } + return new K8sEntity(schema, { + 'entity.definition_id': 'service', + 'entity.type': 'service', + 'kubernetes.service.name': name, + 'kubernetes.namespace': clusterName, + 'entity.id': entityId, + ...others, + }); +} diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/stateful_set.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/stateful_set.ts index 58c603704ebc0..1857471c15635 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/stateful_set.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/stateful_set.ts @@ -27,9 +27,9 @@ export function k8sStatefulSetEntity({ }) { if (schema === 'ecs') { return new K8sEntity(schema, { - 'entity.type': 'stateful_set', + 'entity.definition_id': 'stateful_set', + 'entity.type': 'statefulset', 'kubernetes.statefulset.name': name, - 'kubernetes.statefulset.uid': uid, 'kubernetes.namespace': clusterName, 'entity.id': entityId, ...others, @@ -37,7 +37,8 @@ export function k8sStatefulSetEntity({ } return new K8sEntity(schema, { - 'entity.type': 'stateful_set', + 'entity.definition_id': 'stateful_set', + 'entity.type': 'statefulset', 'k8s.statefulset.name': name, 'k8s.statefulset.uid': uid, 'k8s.cluster.name': clusterName, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts index 8b3ed0cda1072..8fa7a5997f4f7 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts @@ -24,6 +24,7 @@ const defaultLogsOptions: LogsOptions = { export type LogDocument = Fields & Partial<{ + _index?: string; 'input.type': string; 'log.file.path'?: string; 'service.name'?: string; @@ -74,6 +75,14 @@ export type LogDocument = Fields & svc: string; hostname: string; [LONG_FIELD_NAME]: string; + 'http.status_code'?: number; + 'http.request.method'?: string; + 'url.path'?: string; + 'process.name'?: string; + 'kubernetes.namespace'?: string; + 'kubernetes.pod.name'?: string; + 'kubernetes.container.name'?: string; + 'orchestrator.resource.name'?: string; }>; class Log extends Serializable { @@ -155,6 +164,16 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log { ).dataset('synth'); } +function createForIndex(index: string): Log { + return new Log( + { + 'input.type': 'logs', + _index: index, + }, + defaultLogsOptions + ); +} + function createMinimal({ dataset = 'synth', namespace = 'default', @@ -176,6 +195,7 @@ function createMinimal({ export const log = { create, + createForIndex, createMinimal, }; diff --git a/packages/kbn-apm-synthtrace/src/cli/run_synthtrace.ts b/packages/kbn-apm-synthtrace/src/cli/run_synthtrace.ts index d6f899b2084ac..f4646de82d19f 100644 --- a/packages/kbn-apm-synthtrace/src/cli/run_synthtrace.ts +++ b/packages/kbn-apm-synthtrace/src/cli/run_synthtrace.ts @@ -48,6 +48,11 @@ function options(y: Argv) { description: 'Generate and index data continuously', boolean: true, }) + .option('liveBucketSize', { + description: 'Bucket size in ms for live streaming', + default: 1000, + number: true, + }) .option('clean', { describe: 'Clean APM indices before indexing new data', default: false, diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/parse_run_cli_flags.ts b/packages/kbn-apm-synthtrace/src/cli/utils/parse_run_cli_flags.ts index d069f89b168a2..1a7b82dcd364b 100644 --- a/packages/kbn-apm-synthtrace/src/cli/utils/parse_run_cli_flags.ts +++ b/packages/kbn-apm-synthtrace/src/cli/utils/parse_run_cli_flags.ts @@ -74,7 +74,8 @@ export function parseRunCliFlags(flags: RunCliFlags) { 'concurrency', 'versionOverride', 'clean', - 'assume-package-version' + 'assume-package-version', + 'liveBucketSize' ), logLevel: parsedLogLevel, file: parsedFile, diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/start_live_data_upload.ts b/packages/kbn-apm-synthtrace/src/cli/utils/start_live_data_upload.ts index 38404be151612..9478ae8f26af2 100644 --- a/packages/kbn-apm-synthtrace/src/cli/utils/start_live_data_upload.ts +++ b/packages/kbn-apm-synthtrace/src/cli/utils/start_live_data_upload.ts @@ -52,7 +52,7 @@ export async function startLiveDataUpload({ }); } - const bucketSizeInMs = 1000 * 60; + const bucketSizeInMs = runOptions.liveBucketSize; let requestedUntil = start; let currentStreams: PassThrough[] = []; diff --git a/packages/kbn-apm-synthtrace/src/lib/entities/entities_synthtrace_es_client.ts b/packages/kbn-apm-synthtrace/src/lib/entities/entities_synthtrace_es_client.ts index 4c5d17111fca6..624b44eab0887 100644 --- a/packages/kbn-apm-synthtrace/src/lib/entities/entities_synthtrace_es_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/entities/entities_synthtrace_es_client.ts @@ -75,15 +75,13 @@ function getRoutingTransform() { return new Transform({ objectMode: true, transform(document: ESDocumentWithOperation, encoding, callback) { - const entityType: string | undefined = document['entity.type']; - if (entityType === undefined) { - throw new Error(`entity.type was not defined: ${JSON.stringify(document)}`); + const definitionId: string | undefined = document['entity.definition_id']; + if (definitionId === undefined) { + throw new Error(`entity.definition_id was not defined: ${JSON.stringify(document)}`); } - const entityIndexName = `${entityType}s`; document._action = { index: { - _index: - `.entities.v1.latest.builtin_${entityIndexName}_from_ecs_data`.toLocaleLowerCase(), + _index: `.entities.v1.latest.${definitionId}`.toLocaleLowerCase(), _id: document['entity.id'], }, }; diff --git a/packages/kbn-apm-synthtrace/src/lib/shared/data_stream_get_routing_transform.ts b/packages/kbn-apm-synthtrace/src/lib/shared/data_stream_get_routing_transform.ts index 40d1b05878c04..daa631a5ff111 100644 --- a/packages/kbn-apm-synthtrace/src/lib/shared/data_stream_get_routing_transform.ts +++ b/packages/kbn-apm-synthtrace/src/lib/shared/data_stream_get_routing_transform.ts @@ -16,7 +16,7 @@ export function getRoutingTransform(dataStreamType: string) { transform(document: ESDocumentWithOperation, encoding, callback) { if ('data_stream.dataset' in document && 'data_stream.namespace' in document) { document._index = `${dataStreamType}-${document['data_stream.dataset']}-${document['data_stream.namespace']}`; - } else { + } else if (!('_index' in document)) { throw new Error('Cannot determine index for event'); } diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts index 5f3cbd5f054dd..9a8e90f295c61 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts @@ -22,12 +22,351 @@ const { // Arrays for data const LOG_LEVELS: string[] = ['FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']; -const JAVA_LOG_MESSAGES = [ +export const LINUX_PROCESSES = ['cron', 'sshd', 'systemd', 'nginx', 'apache2']; + +// generate 20 short ids to cycle through +const shortIds = Array.from({ length: 20 }, (_, i) => generateShortId()); + +export function getStableShortId() { + return shortIds[Math.floor(Math.random() * shortIds.length)]; +} + +export const getLinuxMessages = () => + ({ + cron: [ + `(${moment().toISOString()}) INFO: (CRON) User ran command: '/usr/bin/backup.sh'.`, + `(${moment().toISOString()}) WARN: (CRON) Missing crontab entry for user .`, + `(${moment().toISOString()}) ERROR: (CRON) Failed to execute '/usr/bin/backup.sh'.`, + `(${moment().toISOString()}) INFO: (CRON) New cron job added for user .`, + `(${moment().toISOString()}) DEBUG: (CRON) Skipping execution of disabled job 'jobID-${getStableShortId()}'.`, + `(${moment().toISOString()}) INFO: (CRON) Daily backup completed successfully in ${Math.floor( + Math.random() * 300 + )} seconds.`, + `(${moment().toISOString()}) ERROR: (CRON) Syntax error in crontab file for user .`, + `(${moment().toISOString()}) INFO: (CRON) Purged old log files during job 'jobID-${getStableShortId()}'.`, + `(${moment().toISOString()}) WARN: (CRON) Job 'jobID-${getStableShortId()}' exceeded timeout of ${Math.floor( + Math.random() * 3600 + )} seconds.`, + `(${moment().toISOString()}) INFO: (CRON) Executing job 'jobID-${getStableShortId()}' as user .`, + ], + sshd: [ + `${moment().toISOString()} INFO: sshd[${Math.floor( + Math.random() * 10000 + )}]: Accepted password for user from ${getIpAddress()} port ${ + 1024 + Math.floor(Math.random() * 50000) + }.`, + `${moment().toISOString()} WARN: sshd[${Math.floor( + Math.random() * 10000 + )}]: Failed password attempt for user from ${getIpAddress()} port ${ + 1024 + Math.floor(Math.random() * 50000) + }.`, + `${moment().toISOString()} INFO: sshd[${Math.floor( + Math.random() * 10000 + )}]: Connection closed by ${getIpAddress()} port ${ + 1024 + Math.floor(Math.random() * 50000) + }.`, + `${moment().toISOString()} ERROR: sshd[${Math.floor( + Math.random() * 10000 + )}]: Invalid public key for user .`, + `${moment().toISOString()} INFO: sshd[${Math.floor( + Math.random() * 10000 + )}]: Starting session for user .`, + `${moment().toISOString()} WARN: sshd[${Math.floor( + Math.random() * 10000 + )}]: Too many authentication failures from ${getIpAddress()}.`, + `${moment().toISOString()} INFO: sshd[${Math.floor( + Math.random() * 10000 + )}]: User disconnected.`, + `${moment().toISOString()} ERROR: sshd[${Math.floor( + Math.random() * 10000 + )}]: Attempt to use forbidden user .`, + `${moment().toISOString()} INFO: sshd[${Math.floor( + Math.random() * 10000 + )}]: Received SIGHUP signal. Reloading configuration.`, + `${moment().toISOString()} DEBUG: sshd[${Math.floor( + Math.random() * 10000 + )}]: Monitoring connections on port 22.`, + ], + systemd: [ + `${moment().toISOString()} INFO: systemd[${Math.floor( + Math.random() * 10000 + )}]: Started service .`, + `${moment().toISOString()} ERROR: systemd[${Math.floor( + Math.random() * 10000 + )}]: Failed to start service .`, + `${moment().toISOString()} INFO: systemd[${Math.floor( + Math.random() * 10000 + )}]: Stopped service .`, + `${moment().toISOString()} DEBUG: systemd[${Math.floor( + Math.random() * 10000 + )}]: Reloading daemon configuration.`, + `${moment().toISOString()} WARN: systemd[${Math.floor( + Math.random() * 10000 + )}]: Service restarted too many times.`, + `${moment().toISOString()} INFO: systemd[${Math.floor( + Math.random() * 10000 + )}]: Mounted .`, + `${moment().toISOString()} ERROR: systemd[${Math.floor( + Math.random() * 10000 + )}]: Unit entered failed state.`, + `${moment().toISOString()} INFO: systemd[${Math.floor( + Math.random() * 10000 + )}]: Timer triggered.`, + `${moment().toISOString()} WARN: systemd[${Math.floor( + Math.random() * 10000 + )}]: Service is inactive.`, + `${moment().toISOString()} DEBUG: systemd[${Math.floor( + Math.random() * 10000 + )}]: Service received SIGTERM.`, + ], + nginx: [ + `${moment().toISOString()} INFO: nginx[${Math.floor( + Math.random() * 10000 + )}]: Access log: ${getIpAddress()} - - [${moment().format( + 'DD/MMM/YYYY:HH:mm:ss Z' + )}] "GET /path-${getStableShortId()} HTTP/1.1" ${ + 200 + Math.floor(Math.random() * 100) + } ${Math.floor(Math.random() * 10000)}.`, + `${moment().toISOString()} ERROR: nginx[${Math.floor( + Math.random() * 10000 + )}]: 502 Bad Gateway for request to /path-${getStableShortId()}.`, + `${moment().toISOString()} WARN: nginx[${Math.floor( + Math.random() * 10000 + )}]: Upstream server timed out on /path-${getStableShortId()}.`, + `${moment().toISOString()} INFO: nginx[${Math.floor( + Math.random() * 10000 + )}]: Server restarted successfully.`, + `${moment().toISOString()} DEBUG: nginx[${Math.floor( + Math.random() * 10000 + )}]: Cache hit for /path-${getStableShortId()}.`, + ], + apache2: [ + `${moment().toISOString()} INFO: apache2[${Math.floor( + Math.random() * 10000 + )}]: GET /path-${getStableShortId()} HTTP/1.1" ${ + 200 + Math.floor(Math.random() * 100) + } ${Math.floor(Math.random() * 10000)} bytes.`, + `${moment().toISOString()} ERROR: apache2[${Math.floor( + Math.random() * 10000 + )}]: 500 Internal Server Error for request /path-${getStableShortId()}.`, + `${moment().toISOString()} WARN: apache2[${Math.floor( + Math.random() * 10000 + )}]: Worker process terminated unexpectedly.`, + `${moment().toISOString()} INFO: apache2[${Math.floor( + Math.random() * 10000 + )}]: Server restarted.`, + `${moment().toISOString()} DEBUG: apache2[${Math.floor( + Math.random() * 10000 + )}]: Keep-alive timeout on connection ${Math.floor(Math.random() * 10000)}.`, + ], + } as Record); + +export const KUBERNETES_SERVICES = [ + 'auth-service', + 'payment-service', + 'inventory-service', + 'ui-service', + 'notification-service', +]; + +export const getKubernetesMessages = () => + ({ + 'auth-service': [ + `User authenticated successfully at ${moment().toISOString()}.`, + `Failed login attempt for user at ${moment().toISOString()}.`, + `Token expired for user .`, + `Session started for user .`, + `Password reset requested by user .`, + `Invalid JWT token provided for user .`, + `New user registered at ${moment().toISOString()}.`, + `MFA challenge triggered for user .`, + `MFA challenge succeeded for user .`, + `MFA challenge failed for user .`, + `Access revoked for user .`, + `User deleted their account.`, + `Permission granted for resource .`, + `Permission denied for resource .`, + `Role updated for user .`, + `User logged out.`, + `Invalid credentials provided for user .`, + `Security alert triggered at ${moment().toISOString()} for user .`, + `Session expired for user .`, + `Password changed successfully for user .`, + ], + 'payment-service': [ + `Payment of $${(Math.random() * 1000).toFixed( + 2 + )} processed successfully at ${moment().toISOString()}.`, + `Card declined for transaction .`, + `Refund initiated for transaction .`, + `Refund of $${(Math.random() * 500).toFixed(2)} processed successfully.`, + `Payment gateway timeout during transaction .`, + `Fraudulent transaction detected at ${moment().toISOString()}.`, + `Payment pending approval for transaction .`, + `Payment gateway configuration error.`, + `Payment of $${(Math.random() * 200).toFixed( + 2 + )} canceled by user .`, + `Recurring payment of $${(Math.random() * 50).toFixed(2)} initiated.`, + `Subscription for renewed successfully.`, + `Subscription for canceled.`, + `Invoice generated.`, + `Invoice sent to user .`, + `Payment method added for user .`, + `Payment method removed for user .`, + `Credit limit exceeded for user .`, + `Insufficient funds for transaction .`, + `Transaction rollback initiated for .`, + `Chargeback received for transaction .`, + ], + 'inventory-service': [ + `Stock level updated for item : ${Math.floor( + Math.random() * 500 + )} units remaining.`, + `Item added to catalog at ${moment().toISOString()}.`, + `Item removed from catalog.`, + `Stock alert for item : Low inventory (${Math.floor( + Math.random() * 20 + )} units left).`, + `Stock replenished for item .`, + `Inventory check completed for warehouse .`, + `Item flagged as discontinued.`, + `Bulk update performed on inventory.`, + `Price updated for item .`, + `Warehouse status: Operational.`, + `Warehouse reported system failure.`, + `Item backordered.`, + `New shipment received for item .`, + `Item sold out.`, + `Item marked for promotion.`, + `Warehouse restocked.`, + `Stock audit started at ${moment().toISOString()}.`, + `Inventory discrepancy reported for item .`, + `Restock delayed for item .`, + `Item reserved for order .`, + ], + 'ui-service': [ + `Page rendered successfully at ${moment().toISOString()}.`, + `User clicked button .`, + `API call to completed in ${Math.floor( + Math.random() * 300 + )}ms.`, + `UI component loaded successfully.`, + `UI component failed to load.`, + `Session timeout for user .`, + `Error rendering component : Invalid data.`, + `User navigated to .`, + `CSS stylesheet loaded.`, + `JavaScript file executed.`, + `UI error at ${moment().toISOString()}: Cannot read property 'undefined'.`, + `Form submitted by user .`, + `Dialog displayed.`, + `Modal closed by user.`, + `Drag-and-drop interaction started.`, + `Drag-and-drop interaction completed.`, + `Keyboard shortcut activated: Ctrl+${String.fromCharCode( + 65 + Math.floor(Math.random() * 26) + )}.`, + `New notification displayed to user .`, + `UI settings updated by user .`, + `User logged out from UI.`, + ], + 'notification-service': [ + `Email sent to user .`, + `Push notification delivered to user .`, + `SMS sent to phone number .`, + `Email delivery failed for user .`, + `Push notification failed for user .`, + `SMS delivery failed for phone number .`, + `User opted out of notifications.`, + `New email template created.`, + `New push notification template created.`, + `New SMS template created.`, + `Batch email sent to ${Math.floor(Math.random() * 500)} recipients.`, + `Batch push notifications sent to ${Math.floor(Math.random() * 500)} recipients.`, + `Batch SMS sent to ${Math.floor(Math.random() * 500)} recipients.`, + `Template deleted.`, + `Notification settings updated for user .`, + `Email verification sent to user .`, + `Password reset notification sent to user .`, + `Marketing email sent to user .`, + `Reminder notification sent to user .`, + `System maintenance notification sent to all users.`, + ], + } as Record); + +export const getJavaMessages = () => [ '[main] com.example1.core.ApplicationCore - Critical failure: NullPointerException encountered during startup', - '[main] com.example.service.UserService - User registration completed for userId: 12345', + '[main] com.example1.core.ApplicationCore - Application started successfully in 3456ms', + '[main] com.example1.core.ApplicationCore - Configuring bean "dataSource" of type HikariCP', + '[main] com.example1.core.ApplicationCore - Memory usage threshold exceeded. GC invoked.', + '[main] com.example1.core.ApplicationCore - Shutting down gracefully on SIGTERM', + + '[main] com.example2.service.PaymentService - Payment processed successfully for orderId: ORD-' + + generateShortId(), + '[main] com.example2.service.PaymentService - Failed to process payment for orderId: ORD-' + + generateShortId() + + '. Reason: Insufficient funds.', + '[main] com.example2.service.PaymentService - Payment gateway timeout for orderId: ORD-' + + generateShortId(), + '[main] com.example2.service.PaymentService - Initiating refund for transactionId: TXN-' + + generateShortId(), + '[main] com.example2.service.PaymentService - Payment retry attempt started for orderId: ORD-' + + generateShortId(), + '[main] com.example3.util.JsonParser - Parsing JSON response from external API', - '[main] com.example4.security.AuthManager - Unauthorized access attempt detected for userId: 67890', + '[main] com.example3.util.JsonParser - Invalid JSON encountered: {"invalid_key":"missing_value"}', + '[main] com.example3.util.JsonParser - Successfully parsed JSON for userId: ' + + Math.floor(Math.random() * 10000), + '[main] com.example3.util.JsonParser - JSON parsing failed due to org.json.JSONException: Unterminated string', + '[main] com.example3.util.JsonParser - Fallback to default configuration triggered due to parsing error', + + '[main] com.example4.security.AuthManager - Unauthorized access attempt detected for userId: ' + + Math.floor(Math.random() * 100000), + '[main] com.example4.security.AuthManager - Password updated for userId: ' + + Math.floor(Math.random() * 100000), + '[main] com.example4.security.AuthManager - User account locked after 3 failed login attempts for userId: ' + + Math.floor(Math.random() * 100000), + '[main] com.example4.security.AuthManager - Token validation failed for token: TOKEN-' + + generateShortId(), + '[main] com.example4.security.AuthManager - User session terminated for userId: ' + + Math.floor(Math.random() * 100000), + '[main] com.example5.dao.UserDao - Database query failed: java.sql.SQLException: Timeout expired', + '[main] com.example5.dao.UserDao - Retrieved 10 results for query: SELECT * FROM users WHERE status = "active"', + '[main] com.example5.dao.UserDao - Connection pool exhausted. Waiting for available connection.', + '[main] com.example5.dao.UserDao - Insert operation succeeded for userId: ' + + Math.floor(Math.random() * 100000), + '[main] com.example5.dao.UserDao - Detected stale connection. Retrying operation.', + + '[main] com.example6.metrics.MetricsCollector - Reporting CPU usage: ' + + (Math.random() * 100).toFixed(2) + + '%', + '[main] com.example6.metrics.MetricsCollector - Application uptime: ' + + Math.floor(Math.random() * 86400) + + ' seconds', + '[main] com.example6.metrics.MetricsCollector - Memory usage: Heap=128MB Non-Heap=64MB', + '[main] com.example6.metrics.MetricsCollector - GC activity detected. Time taken: ' + + Math.floor(Math.random() * 100) + + 'ms', + '[main] com.example6.metrics.MetricsCollector - Collected metrics for 15 services', + + '[main] com.example7.messaging.MessageQueue - Message published to queue "orders" with messageId: MSG-' + + generateShortId(), + '[main] com.example7.messaging.MessageQueue - Consumer failed to process messageId: MSG-' + + generateShortId() + + '. Error: NullPointerException', + '[main] com.example7.messaging.MessageQueue - Queue "notifications" has 50 pending messages', + '[main] com.example7.messaging.MessageQueue - Retrying message delivery for messageId: MSG-' + + generateShortId(), + '[main] com.example7.messaging.MessageQueue - Dead-letter queue reached maximum size. Oldest messages purged.', + + '[main] com.example8.integration.ExternalServiceClient - HTTP 200: Successfully received response from "https://api.example.com/v1/resource"', + '[main] com.example8.integration.ExternalServiceClient - HTTP 500: Internal Server Error while accessing "https://api.example.com/v1/resource"', + '[main] com.example8.integration.ExternalServiceClient - Connection timeout occurred after 30 seconds', + '[main] com.example8.integration.ExternalServiceClient - Retrying request to endpoint "https://api.example.com/v1/resource"', + '[main] com.example8.integration.ExternalServiceClient - API key validation failed for key: APIKEY-' + + generateShortId(), ]; const IP_ADDRESSES = [ @@ -71,14 +410,25 @@ export const getCloudRegion = (index?: number) => getAtIndexOrRandom(CLOUD_REGIO export const getServiceName = (index?: number) => getAtIndexOrRandom(SERVICE_NAMES, index); export const getAgentName = (index?: number) => getAtIndexOrRandom(ELASTIC_AGENT_NAMES, index); -export const getJavaLog = () => - `${moment().format('YYYY-MM-DD HH:mm:ss,SSS')} ${getAtIndexOrRandom( - LOG_LEVELS - )} ${getAtIndexOrRandom(JAVA_LOG_MESSAGES)}`; +export const getJavaLogs = () => { + const javaLogMessages = getJavaMessages(); + return getRandomRange().map( + () => + `${moment().format('YYYY-MM-DD HH:mm:ss,SSS')} ${getAtIndexOrRandom( + LOG_LEVELS + )} ${getAtIndexOrRandom(javaLogMessages)}` + ); +}; + +export function getRandomRange() { + return Array.from({ length: Math.floor(Math.random() * 1000) + 1 }).fill(null); +} -export const getWebLog = () => { - const path = `/api/${noun()}/${verb()}`; - const bytes = randomInt(100, 4000); +export const getWebLogs = () => { + return getRandomRange().map(() => { + const path = `/api/${noun()}/${verb()}`; + const bytes = randomInt(100, 4000); - return `${ipv4()} - - [${moment().toISOString()}] "${httpMethod()} ${path} HTTP/1.1" ${httpStatusCode()} ${bytes} "-" "${userAgent()}"`; + return `${ipv4()} - - [${moment().toISOString()}] "${httpMethod()} ${path} HTTP/1.1" ${httpStatusCode()} ${bytes} "-" "${userAgent()}"`; + }); }; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/k8s_entities.ts b/packages/kbn-apm-synthtrace/src/scenarios/k8s_entities.ts index 7d94cc3180a7e..8300d20589827 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/k8s_entities.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/k8s_entities.ts @@ -31,6 +31,7 @@ const CRON_JOB_ENTITY_ID = generateShortId(); const CRON_JOB_UID = generateShortId(); const NODE_ENTITY_ID = generateShortId(); const NODE_UID = generateShortId(); +const SERVICE_UID = generateShortId(); const scenario: Scenario> = async (runOptions) => { const { logger } = runOptions; @@ -45,7 +46,7 @@ const scenario: Scenario> = async (runOptions) => { .interval('1m') .rate(1) .generator((timestamp) => { - return [ + const commonEntities = [ entities.k8s .k8sClusterJobEntity({ schema, @@ -99,7 +100,7 @@ const scenario: Scenario> = async (runOptions) => { }) .timestamp(timestamp), entities.k8s - .k8sJobSetEntity({ + .k8sJobEntity({ clusterName: CLUSTER_NAME, name: 'job_set_foo', schema, @@ -133,10 +134,26 @@ const scenario: Scenario> = async (runOptions) => { }) .timestamp(timestamp), ]; + + if (schema === 'ecs') { + return [ + ...commonEntities, + entities.k8s + .k8sServiceEntity({ + schema, + clusterName: CLUSTER_NAME, + name: 'my_service', + entityId: SERVICE_UID, + }) + .timestamp(timestamp), + ]; + } + + return commonEntities; }); const ecsEntities = getK8sEntitiesEvents('ecs'); - const otelEntities = getK8sEntitiesEvents('semconv'); + const otelEntities = getK8sEntitiesEvents('otel'); return [ withClient( diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_entities.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_entities.ts index 8b0d2afa5a971..4aa59d3ee8409 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_entities.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_entities.ts @@ -100,7 +100,7 @@ const scenario: Scenario> = async (runOptions) => { }) .timestamp(timestamp), entities.k8s - .k8sJobSetEntity({ + .k8sJobEntity({ clusterName: CLUSTER_NAME, name: 'job_set_foo', schema, @@ -137,7 +137,7 @@ const scenario: Scenario> = async (runOptions) => { }); const ecsEntities = getK8sEntitiesEvents('ecs'); - const otelEntities = getK8sEntitiesEvents('semconv'); + const otelEntities = getK8sEntitiesEvents('otel'); const synthJavaTraces = entities.serviceEntity({ serviceName: 'synth_java', agentName: ['java'], diff --git a/packages/kbn-apm-synthtrace/src/scenarios/slash_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/slash_logs.ts new file mode 100644 index 0000000000000..26c998f658661 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/slash_logs.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { LogDocument, generateShortId, log } from '@kbn/apm-synthtrace-client'; +import { Scenario } from '../cli/scenario'; +import { withClient } from '../lib/utils/with_client'; +import { + getAgentName, + getCloudProvider, + getCloudRegion, + getIpAddress, + getJavaLogs, + getServiceName, + getWebLogs, + getKubernetesMessages, + getLinuxMessages, + KUBERNETES_SERVICES, + getStableShortId, + getRandomRange, +} from './helpers/logs_mock_data'; +import { getAtIndexOrRandom } from './helpers/get_at_index_or_random'; + +const LINUX_PROCESSES = ['cron', 'sshd', 'systemd', 'nginx', 'apache2']; + +const scenario: Scenario = async (runOptions) => { + const constructCommonMetadata = () => ({ + 'agent.name': getAgentName(), + 'cloud.provider': getCloudProvider(), + 'cloud.region': getCloudRegion(Math.floor(Math.random() * 3)), + 'cloud.availability_zone': `${getCloudRegion(0)}a`, + 'cloud.instance.id': generateShortId(), + 'cloud.project.id': generateShortId(), + }); + + const generateNginxLogs = (timestamp: number) => { + return getWebLogs().map((message) => { + return log + .createForIndex('logs') + .setHostIp(getIpAddress()) + .message(message) + .defaults({ + ...constructCommonMetadata(), + 'log.file.path': `/var/log/nginx/access-${getStableShortId()}.log`, + }) + .timestamp(timestamp); + }); + }; + + const generateSyslogData = (timestamp: number) => { + const messages: Record = getLinuxMessages(); + + return getRandomRange().map(() => { + const processName = getAtIndexOrRandom(LINUX_PROCESSES); + const message = getAtIndexOrRandom(messages[processName]); + return log + .createForIndex('logs') + .message(message) + .setHostIp(getIpAddress()) + .defaults({ + ...constructCommonMetadata(), + 'process.name': processName, + 'log.file.path': `/var/log/${processName}.log`, + }) + .timestamp(timestamp); + }); + }; + + const generateKubernetesLogs = (timestamp: number) => { + const messages: Record = getKubernetesMessages(); + + return getRandomRange().map(() => { + const service = getAtIndexOrRandom(KUBERNETES_SERVICES); + const isStringifiedJSON = Math.random() > 0.5; + const message = isStringifiedJSON + ? JSON.stringify({ + serviceName: service, + message: getAtIndexOrRandom(messages[service]), + }) + : getAtIndexOrRandom(messages[service]); + return log + .createForIndex('logs') + .message(message) + .setHostIp(getIpAddress()) + .defaults({ + ...constructCommonMetadata(), + 'kubernetes.namespace': 'default', + 'kubernetes.pod.name': `${service}-pod-${getStableShortId()}`, + 'kubernetes.container.name': `${service}-container`, + 'orchestrator.resource.name': service, + }) + .timestamp(timestamp); + }); + }; + + const generateUnparsedJavaLogs = (timestamp: number) => { + return getJavaLogs().map((message) => { + const serviceName = getServiceName(Math.floor(Math.random() * 3)); + return log + .createForIndex('logs') + .message(message) + .defaults({ + ...constructCommonMetadata(), + 'service.name': serviceName, + }) + .timestamp(timestamp); + }); + }; + + return { + generate: ({ range, clients: { logsEsClient } }) => { + const { logger } = runOptions; + + const nginxLogs = range.interval('1m').generator(generateNginxLogs); + const syslogData = range.interval('1m').generator(generateSyslogData); + const kubernetesLogs = range.interval('1m').generator(generateKubernetesLogs); + const unparsedJavaLogs = range.interval('1m').generator(generateUnparsedJavaLogs); + + return withClient( + logsEsClient, + logger.perf('generating_messy_logs', () => [ + nginxLogs, + syslogData, + kubernetesLogs, + unparsedJavaLogs, + ]) + ); + }, + }; +}; + +export default scenario; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/unstructured_logs.ts index 704cfd21bbc09..b87fd7038a7d3 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/unstructured_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/unstructured_logs.ts @@ -12,7 +12,7 @@ import { Scenario } from '../cli/scenario'; import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; import { withClient } from '../lib/utils/with_client'; import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; -import { getJavaLog, getWebLog } from './helpers/logs_mock_data'; +import { getJavaLogs, getWebLogs } from './helpers/logs_mock_data'; const scenario: Scenario = async (runOptions) => { const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts); @@ -23,19 +23,18 @@ const scenario: Scenario = async (runOptions) => { generate: ({ range, clients: { logsEsClient } }) => { const { logger } = runOptions; - const datasetJavaLogs = (timestamp: number) => - log.create({ isLogsDb }).dataset('java').message(getJavaLog()).timestamp(timestamp); - - const datasetWebLogs = (timestamp: number) => - log.create({ isLogsDb }).dataset('web').message(getWebLog()).timestamp(timestamp); - const logs = range .interval('1m') .rate(1) .generator((timestamp) => { - return Array(200) - .fill(0) - .flatMap((_, index) => [datasetJavaLogs(timestamp), datasetWebLogs(timestamp)]); + return [ + ...getJavaLogs().map((message) => + log.create({ isLogsDb }).dataset('java').message(message).timestamp(timestamp) + ), + ...getWebLogs().map((message) => + log.create({ isLogsDb }).dataset('web').message(message).timestamp(timestamp) + ), + ]; }); return withClient( diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 619bdd6c29321..45313933ab1b4 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -511,6 +511,7 @@ ], "fleet-message-signing-keys": [], "fleet-package-policies": [ + "bump_agent_policy_revision", "created_at", "created_by", "description", @@ -532,6 +533,7 @@ "revision", "secret_references", "secret_references.id", + "supports_agentless", "updated_at", "updated_by", "vars" @@ -692,6 +694,7 @@ "version" ], "ingest-package-policies": [ + "bump_agent_policy_revision", "created_at", "created_by", "description", @@ -713,6 +716,7 @@ "revision", "secret_references", "secret_references.id", + "supports_agentless", "updated_at", "updated_by", "vars" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index d6ec30393e099..17fb2c8cff1ed 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1715,6 +1715,9 @@ }, "fleet-package-policies": { "properties": { + "bump_agent_policy_revision": { + "type": "boolean" + }, "created_at": { "type": "date" }, @@ -1783,6 +1786,9 @@ } } }, + "supports_agentless": { + "type": "boolean" + }, "updated_at": { "type": "date" }, @@ -2300,6 +2306,9 @@ }, "ingest-package-policies": { "properties": { + "bump_agent_policy_revision": { + "type": "boolean" + }, "created_at": { "type": "date" }, @@ -2368,6 +2377,9 @@ } } }, + "supports_agentless": { + "type": "boolean" + }, "updated_at": { "type": "date" }, diff --git a/packages/kbn-dependency-usage/README.md b/packages/kbn-dependency-usage/README.md new file mode 100644 index 0000000000000..95d0b237bb141 --- /dev/null +++ b/packages/kbn-dependency-usage/README.md @@ -0,0 +1,153 @@ + +# @kbn/dependency-usage + +A CLI tool for analyzing dependencies across packages and plugins. This tool provides commands to check dependency usage, aggregate it, debug dependency graphs, and more. + +--- + +## Table of Contents +1. [Show all packages/plugins using a dependency](#show-all-packagesplugins-using-a-dependency) +2. [Show dependencies grouped by code owner](#show-dependencies-grouped-by-code-owner) +3. [List all dependencies for a package or directory](#list-all-dependencies-for-source-directory) +4. [Group by code owner with adjustable collapse depth](#group-by-code-owner-with-adjustable-collapse-depth) +5. [Show dependencies matching a pattern](#show-dependencies-matching-a-pattern) +6. [Verbose flag to debug dependency graph issues](#verbose-flag-to-debug-dependency-graph-issues) + +--- + + +### 1. Show all packages/plugins using a specific dependency + +Use this command to list all packages or plugins within a directory that use a specified dependency. + +```sh +bash scripts/dependency_usage.sh -d -p +``` +or +```sh +bash scripts/dependency_usage.sh --dependency-name --paths +``` + +**Example**: +```sh +bash scripts/dependency_usage.sh -d rxjs -p x-pack/plugins/security_solution +``` + +- `-d rxjs`: Specifies the dependency to look for (`rxjs`). +- `-p x-pack/plugins/security_solution`: Sets the directory to search within (`x-pack/plugins/security_solution`). + +--- + +### 2. Show dependencies grouped by code owner + +Group the dependencies used within a directory by code owner. + +```sh +bash scripts/dependency_usage.sh -p -g owner +``` +or +```sh +bash scripts/dependency_usage.sh --paths --group-by owner +``` + +**Example**: +```sh +bash scripts/dependency_usage.sh -p x-pack/plugins -g owner +``` + +- `-p x-pack/plugins`: Sets the directory to scan for plugins using this dependency. +- `-g owner`: Groups results by code owner. +- **Output**: Lists all dependencies for `x-pack/plugins`, organized by code owner. + +--- + +### 3. List all dependencies for source directory + +To display all dependencies used within a specific directory. + +```sh +bash scripts/dependency_usage.sh -p +``` +or +```sh +bash scripts/dependency_usage.sh --paths +``` + +**Example**: +```sh +bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution +``` + +- `-p x-pack/plugins/security_solution`: Specifies the package or directory for which to list all dependencies. +- **Output**: Lists all dependencies for `x-pack/plugins/security_solution`. + +--- + +### 4. Group by code owner with adjustable collapse depth + +When a package or plugin has multiple subteams, use the `--collapse-depth` option to control how granular the grouping by code owner should be. + +#### Detailed Subteam Grouping +Shows all subteams within `security_solution`. + +```sh +bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 4 +``` + +#### Collapsed Grouping +Groups the results under a higher-level owner (e.g., `security_solution` as a single group). + +```sh +bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -g owner --collapse-depth 1 +``` + +**Explanation**: +- `-p x-pack/plugins/security_solution`: Specifies the directory to scan. +- `-g owner`: Groups results by code owner. +- `--collapse-depth`: Defines the depth for grouping, where higher numbers show more granular subteams. +- **Output**: Lists dependencies grouped by code owner at different levels of depth based on the `--collapse-depth` value. + +--- + +### 5. Show dependencies matching a pattern + +Search for dependencies that match a specific pattern (such as `react-*`) within a package and output the results to a specified file. + +```sh +bash scripts/dependency_usage.sh -p -d '' -o +``` + +**Example**: +```sh +bash scripts/dependency_usage.sh -d 'react-*' -p x-pack/plugins/security_solution -o ./tmp/results.json +``` + +- `-p x-pack/plugins/security_solution`: Specifies the directory or package to search within. +- `-d 'react-*'`: Searches for dependencies that match the pattern `react-*`. +- `-o ./tmp/results.json`: Outputs the results to a specified file (`results.json` in the `./tmp` directory). +- **Output**: Saves a list of all dependencies matching `react-*` in `x-pack/plugins/security_solution` to `./tmp/results.json`. + +--- + +### 6. Verbose flag to debug dependency graph issues + +Enable verbose mode to log additional details for debugging dependency graphs. This includes generating a non-aggregated dependency graph in `.dependency-graph-log.json`. + +```sh +bash scripts/dependency_usage.sh -p -o -v +``` + +**Example**: +```sh +bash scripts/dependency_usage.sh -p x-pack/plugins/security_solution -o ./tmp/results.json +``` +- `-p x-pack/plugins/security_solution`: Specifies the target directory or package to analyze. +- `-o ./tmp/results.json`: Saves the output to the `results.json` file in the `./tmp` directory. +- `-v`: Enables verbose mode. + +**Output**: Saves a list of all dependencies in `x-pack/plugins/security_solution` to `./tmp/results.json`. Additionally, it logs a detailed, non aggregated dependency graph to `.dependency-graph-log.json` for debugging purposes. + +--- + +For further information on additional flags and options, refer to the script's help command. + diff --git a/packages/kbn-dependency-usage/jest.config.js b/packages/kbn-dependency-usage/jest.config.js new file mode 100644 index 0000000000000..4a579a5ff94b8 --- /dev/null +++ b/packages/kbn-dependency-usage/jest.config.js @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/* eslint-disable no-restricted-syntax */ +export default { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-dependency-usage'], +}; diff --git a/packages/kbn-dependency-usage/kibana.jsonc b/packages/kbn-dependency-usage/kibana.jsonc new file mode 100644 index 0000000000000..b5d8f6261eee8 --- /dev/null +++ b/packages/kbn-dependency-usage/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "devOnly": true, + "type": "shared-common", + "id": "@kbn/dependency-usage", + "owner": "@elastic/kibana-security" +} diff --git a/packages/kbn-dependency-usage/package.json b/packages/kbn-dependency-usage/package.json new file mode 100644 index 0000000000000..631d012028587 --- /dev/null +++ b/packages/kbn-dependency-usage/package.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "name": "@kbn/dependency-usage", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "type": "module", + "exports": { + "./src/*": "./src/*" + } +} \ No newline at end of file diff --git a/packages/kbn-dependency-usage/src/cli.test.ts b/packages/kbn-dependency-usage/src/cli.test.ts new file mode 100644 index 0000000000000..5e40e4e39619c --- /dev/null +++ b/packages/kbn-dependency-usage/src/cli.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { identifyDependencyUsageWithCruiser } from './dependency_graph/providers/cruiser.ts'; +import { configureYargs } from './cli'; + +jest.mock('chalk', () => ({ + green: jest.fn((str) => str), + yellow: jest.fn((str) => str), + cyan: jest.fn((str) => str), + magenta: jest.fn((str) => str), + blue: jest.fn((str) => str), + bold: { magenta: jest.fn((str) => str), blue: jest.fn((str) => str) }, +})); + +jest.mock('./dependency_graph/providers/cruiser', () => ({ + identifyDependencyUsageWithCruiser: jest.fn(), +})); + +jest.mock('./cli', () => ({ + ...jest.requireActual('./cli'), + runCLI: jest.fn(), +})); + +describe('dependency-usage CLI', () => { + const parser = configureYargs() + .fail((message: string) => { + throw new Error(message); + }) + .exitProcess(false); + + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should handle verbose option', () => { + const argv = parser.parse(['--paths', './plugins', '--verbose']); + expect(argv.verbose).toBe(true); + + expect(identifyDependencyUsageWithCruiser).toHaveBeenCalledWith( + expect.any(Array), + undefined, + expect.objectContaining({ isVerbose: true }) + ); + }); + + it('should group results by specified group-by option', () => { + const argv = parser.parse(['--paths', './src', '--group-by', 'owner']); + expect(argv['group-by']).toBe('owner'); + + expect(identifyDependencyUsageWithCruiser).toHaveBeenCalledWith( + expect.any(Array), + undefined, + expect.objectContaining({ groupBy: 'owner' }) + ); + }); + + it('should use default values when optional arguments are not provided', () => { + const argv = parser.parse([]); + expect(argv.paths).toEqual(['.']); + expect(argv['dependency-name']).toBeUndefined(); + expect(argv['collapse-depth']).toBe(1); + expect(argv.verbose).toBe(false); + }); + + it('should throw an error if summary is used without dependency-name', () => { + expect(() => { + parser.parse(['--summary', '--paths', './src']); + }).toThrow('Summary option can only be used when a dependency name is provided'); + }); + + it('should validate collapse-depth as a positive integer', () => { + expect(() => { + parser.parse(['--paths', './src', '--collapse-depth', '0']); + }).toThrow('Collapse depth must be a positive integer'); + }); + + it('should output results to specified output path', () => { + const argv = parser.parse(['--paths', './src', '--output-path', './output.json']); + expect(argv['output-path']).toBe('./output.json'); + }); + + it('should print results to console if no output path is specified', () => { + const argv = parser.parse(['--paths', './src']); + expect(argv['output-path']).toBeUndefined(); + }); +}); diff --git a/packages/kbn-dependency-usage/src/cli.ts b/packages/kbn-dependency-usage/src/cli.ts new file mode 100644 index 0000000000000..674150fa4d91e --- /dev/null +++ b/packages/kbn-dependency-usage/src/cli.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import nodePath from 'path'; +import yargs from 'yargs'; +import chalk from 'chalk'; +import fs from 'fs'; + +import { identifyDependencyUsageWithCruiser } from './dependency_graph/providers/cruiser.ts'; + +interface CLIArgs { + dependencyName?: string; + paths: string[]; + groupBy: string; + summary: boolean; + outputPath: string; + collapseDepth: number; + tool: string; + verbose: boolean; +} + +export const configureYargs = () => { + return yargs(process.argv.slice(2)) + .command( + '*', + chalk.green('Identify the usage of a dependency in the given paths and output as JSON'), + (y) => { + y.version(false) + .option('dependency-name', { + alias: 'd', + describe: chalk.yellow('The name of the dependency to search for'), + type: 'string', + demandOption: false, + }) + .option('paths', { + alias: 'p', + describe: chalk.cyan('The paths to search within (can be multiple)'), + type: 'string', + array: true, + default: ['.'], + }) + .option('group-by', { + alias: 'g', + describe: chalk.magenta('Group results by either owner or source (package/plugin)'), + choices: ['owner', 'source'], + }) + .option('summary', { + alias: 's', + describe: chalk.magenta( + 'Output a summary instead of full details. Applies only when a dependency name is provided' + ), + type: 'boolean', + }) + .option('collapse-depth', { + alias: 'c', + describe: chalk.blue('Specify the directory depth level for collapsing'), + type: 'number', + default: 1, + }) + .option('output-path', { + alias: 'o', + describe: chalk.blue('Specify the output file to save results as JSON'), + type: 'string', + }) + .option('verbose', { + alias: 'v', + describe: chalk.blue('Outputs verbose graph details to a file'), + type: 'boolean', + default: false, + }) + .check(({ summary, dependencyName, collapseDepth }: Partial) => { + if (summary && !dependencyName) { + throw new Error('Summary option can only be used when a dependency name is provided'); + } + + if (collapseDepth !== undefined && collapseDepth <= 0) { + throw new Error('Collapse depth must be a positive integer'); + } + + return true; + }) + .example( + '--dependency-name lodash --paths ./src ./lib', + chalk.blue( + 'Searches for "lodash" usage in the ./src and ./lib directories and outputs as JSON' + ) + ); + }, + async (argv: CLIArgs) => { + const { + dependencyName, + paths, + groupBy, + summary, + collapseDepth, + outputPath, + verbose: isVerbose, + } = argv; + if (dependencyName) { + console.log( + `Searching for dependency ${chalk.bold.magenta( + dependencyName + )} in paths: ${chalk.bold.magenta(paths.join(', '))}` + ); + } else { + console.log( + `Searching for dependencies in paths: ${chalk.bold.magenta(paths.join(', '))}` + ); + } + + if (collapseDepth > 1) { + console.log(`Dependencies will be collapsed to depth: ${chalk.bold.blue(collapseDepth)}`); + } + + try { + console.log(`${chalk.bold.magenta('cruiser')} is used for building dependency graph`); + + const result = await identifyDependencyUsageWithCruiser(paths, dependencyName, { + groupBy, + summary, + collapseDepth, + isVerbose, + }); + + if (outputPath) { + const isJsonFile = nodePath.extname(outputPath) === '.json'; + const outputFile = isJsonFile + ? outputPath + : nodePath.join(outputPath, 'dependency-usage.json'); + + const outputDir = nodePath.dirname(outputFile); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFile(outputFile, JSON.stringify(result, null, 2), (err) => { + if (err) { + console.error(chalk.red(`Failed to save results to ${outputFile}: ${err.message}`)); + } else { + console.log(chalk.green(`Results successfully saved to ${outputFile}`)); + } + }); + } else { + console.log(chalk.yellow('No output file specified, displaying results below:\n')); + console.log(JSON.stringify(result, null, 2)); + } + } catch (error) { + console.error('Error fetching dependency usage:', error.message); + } + } + ) + .help(); +}; + +export const runCLI = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + configureYargs().argv; +}; + +if (!process.env.JEST_WORKER_ID) { + runCLI(); +} diff --git a/packages/kbn-dependency-usage/src/dependency_graph/common/constants.ts b/packages/kbn-dependency-usage/src/dependency_graph/common/constants.ts new file mode 100644 index 0000000000000..e13a696450328 --- /dev/null +++ b/packages/kbn-dependency-usage/src/dependency_graph/common/constants.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const aggregationGroups = [ + 'x-pack/plugins', + 'x-pack/packages', + 'src/plugins', + 'packages', + 'src', + 'x-pack/test', + 'x-pack/test_serverless', +]; + +export const excludePaths = [ + '(^|/)target($|/)', + '^kbn', + '^@kbn', + '^.buildkite', + '^docs', + '^dev_docs', + '^examples', + '^scripts', + '^bazel', + '^x-pack/examples', + '^oas_docs', + '^api_docs', + '^kbn_pm', + '^.es', + '^.codeql', + '^.github', +]; diff --git a/packages/kbn-dependency-usage/src/dependency_graph/index.ts b/packages/kbn-dependency-usage/src/dependency_graph/index.ts new file mode 100644 index 0000000000000..c02e90eb1edeb --- /dev/null +++ b/packages/kbn-dependency-usage/src/dependency_graph/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { identifyDependencyUsageWithCruiser } from './providers/cruiser.ts'; diff --git a/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.test.ts b/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.test.ts new file mode 100644 index 0000000000000..ed2004c462ab3 --- /dev/null +++ b/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.test.ts @@ -0,0 +1,354 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { identifyDependencyUsageWithCruiser as identifyDependencyUsage } from './cruiser.ts'; +import { cruise } from 'dependency-cruiser'; + +import * as groupBy from '../../lib/group_by_owners.ts'; +import * as groupBySource from '../../lib/group_by_source.ts'; + +const codeOwners: Record = { + 'plugins/security': ['team_security'], + 'plugins/data_visualization': ['team_visualization'], + 'plugins/data_charts': ['team_visualization'], + 'plugins/analytics': ['team_analytics'], + 'plugins/notification': ['team_alerts', 'team_notifications'], + 'plugins/security_solution/public/entity_analytics/components': ['team_security_analytics'], + 'plugins/security_solution/public/entity_analytics/components/componentA.ts': [ + 'team_security_analytics', + ], + 'plugins/security_solution/public/entity_analytics/components/componentB.ts': [ + 'team_security_analytics', + ], + 'plugins/security_solution/server/lib/analytics/analytics.ts': ['team_security_analytics'], + 'plugins/security_solution/common/api/detection_engine': ['team_security_solution'], +}; + +jest.mock('dependency-cruiser', () => ({ + cruise: jest.fn(), +})); + +const mockCruiseResult = { + output: { + summary: { + violations: [ + { + from: 'plugins/security', + to: 'node_modules/rxjs', + }, + { + from: 'plugins/data_visualization', + to: 'node_modules/rxjs', + }, + { + from: 'plugins/data_charts', + to: 'node_modules/rxjs', + }, + { + from: 'plugins/analytics', + to: 'node_modules/rxjs', + }, + { + from: 'plugins/analytics', + to: 'node_modules/@hapi/boom', + }, + ], + }, + modules: [ + { + source: 'node_modules/rxjs', + dependents: [ + 'plugins/security/server/index.ts', + 'plugins/data_charts/public/charts.ts', + 'plugins/data_visualization/public/visualization.ts', + 'plugins/data_visualization/public/ingest.ts', + 'plugins/analytics/server/analytics.ts', + ], + }, + { + source: 'node_modules/@hapi/boom', + dependents: ['plugins/analytics'], + }, + ], + }, +}; + +jest.mock('../../lib/code_owners', () => ({ + getCodeOwnersForFile: jest.fn().mockImplementation((filePath: string) => codeOwners[filePath]), + getPathsWithOwnersReversed: () => ({}), +})); + +describe('identifyDependencyUsage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should respect collapseDepth param', async () => { + (cruise as jest.Mock).mockResolvedValue(mockCruiseResult); + + await identifyDependencyUsage([], 'rxjs', { + groupBy: 'owner', + collapseDepth: 2, + summary: false, + }); + + await identifyDependencyUsage([], undefined, { + groupBy: 'owner', + collapseDepth: 1, + summary: false, + }); + + const [, configWithDepth2] = (cruise as jest.Mock).mock.calls[0]; + const [, configWithDepth1] = (cruise as jest.Mock).mock.calls[1]; + + expect(configWithDepth2.collapse).toMatchInlineSnapshot( + `"^(x-pack/plugins|x-pack/packages|src/plugins|packages|src|x-pack/test|x-pack/test_serverless)/([^/]+)/([^/]+)"` + ); + + expect(configWithDepth1.collapse).toMatchInlineSnapshot( + `"^(x-pack/plugins|x-pack/packages|src/plugins|packages|src|x-pack/test|x-pack/test_serverless)/([^/]+)|^node_modules/(@[^/]+/[^/]+|[^/]+)"` + ); + }); + + it('should group dependencies by codeowners', async () => { + (cruise as jest.Mock).mockResolvedValue(mockCruiseResult); + const groupFilesByOwnersSpy = jest.spyOn(groupBy, 'groupFilesByOwners'); + + const result = await identifyDependencyUsage([], undefined, { + groupBy: 'owner', + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(groupFilesByOwnersSpy).toHaveBeenCalledWith(mockCruiseResult.output.summary.violations); + + expect(result).toEqual({ + team_security: { + modules: ['plugins/security'], + deps: ['rxjs'], + teams: ['team_security'], + }, + team_visualization: { + modules: ['plugins/data_visualization', 'plugins/data_charts'], + deps: ['rxjs'], + teams: ['team_visualization'], + }, + team_analytics: { + modules: ['plugins/analytics'], + deps: ['rxjs', '@hapi/boom'], + teams: ['team_analytics'], + }, + }); + }); + + it('should group dependencies by source directory', async () => { + (cruise as jest.Mock).mockResolvedValue(mockCruiseResult); + const groupFilesByOwnersSpy = jest.spyOn(groupBySource, 'groupBySource'); + + const result = await identifyDependencyUsage([], undefined, { + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(groupFilesByOwnersSpy).toHaveBeenCalledWith(mockCruiseResult.output.summary.violations); + + expect(result).toEqual({ + 'plugins/security': ['rxjs'], + 'plugins/data_visualization': ['rxjs'], + 'plugins/data_charts': ['rxjs'], + 'plugins/analytics': ['rxjs', '@hapi/boom'], + }); + }); + + it('should search for specific dependency and return full dependents list', async () => { + (cruise as jest.Mock).mockResolvedValue(mockCruiseResult); + const result = await identifyDependencyUsage([], 'rxjs', { + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + + expect(result).toEqual({ + modules: [ + 'plugins/security', + 'plugins/data_visualization', + 'plugins/data_charts', + 'plugins/analytics', + ], + dependents: { + rxjs: [ + 'plugins/security/server/index.ts', + 'plugins/data_charts/public/charts.ts', + 'plugins/data_visualization/public/visualization.ts', + 'plugins/data_visualization/public/ingest.ts', + 'plugins/analytics/server/analytics.ts', + ], + }, + }); + }); + + it('should search for specific dependency and return only summary', async () => { + (cruise as jest.Mock).mockResolvedValue(mockCruiseResult); + const result = await identifyDependencyUsage([], 'rxjs', { + collapseDepth: 1, + summary: true, + }); + + expect(cruise).toHaveBeenCalled(); + + expect(result).toEqual({ + modules: [ + 'plugins/security', + 'plugins/data_visualization', + 'plugins/data_charts', + 'plugins/analytics', + ], + }); + }); + + it('should handle empty cruise result', async () => { + (cruise as jest.Mock).mockResolvedValue({ + output: { summary: { violations: [] }, modules: [] }, + }); + + const result = await identifyDependencyUsage([], undefined, { + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(result).toEqual({}); + }); + + it('should handle no violations', async () => { + (cruise as jest.Mock).mockResolvedValue({ + output: { summary: { violations: [] }, modules: mockCruiseResult.output.modules }, + }); + + const result = await identifyDependencyUsage([], undefined, { + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(result).toEqual({}); + }); + + it('should return empty structure if specific dependency name does not exist', async () => { + (cruise as jest.Mock).mockResolvedValue({ + output: { summary: { violations: [] }, modules: mockCruiseResult.output.modules }, + }); + + const result = await identifyDependencyUsage([], 'nonexistent_dependency', { + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(result).toEqual({ + modules: [], + dependents: {}, + }); + }); + + it('should handle unknown ownership when grouping by owner', async () => { + const customCruiseResult = { + output: { + summary: { + violations: [ + { from: 'plugins/unknown_plugin', to: 'node_modules/some_module' }, + { from: 'plugins/security', to: 'node_modules/rxjs' }, + ], + }, + modules: [], + }, + }; + (cruise as jest.Mock).mockResolvedValue(customCruiseResult); + + const result = await identifyDependencyUsage([], undefined, { + groupBy: 'owner', + collapseDepth: 1, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(result).toEqual({ + unknown: { + modules: ['plugins/unknown_plugin'], + deps: ['some_module'], + teams: ['unknown'], + }, + team_security: { + modules: ['plugins/security'], + deps: ['rxjs'], + teams: ['team_security'], + }, + }); + }); + + it('should search for specific dependency and group by owner', async () => { + const customCruiseResult = { + output: { + summary: { + violations: [ + { + from: 'plugins/security_solution/public/entity_analytics/components/componentA.ts', + to: 'node_modules/lodash/fp.js', + }, + { + from: 'plugins/security_solution/public/entity_analytics/components/componentB.ts', + to: 'node_modules/lodash/partition.js', + }, + { + from: 'plugins/security_solution/server/lib/analytics/analytics.ts', + to: 'node_modules/lodash/partition.js', + }, + { + from: 'plugins/security_solution/server/lib/analytics/analytics.ts', + to: 'node_modules/lodash/cloneDeep.js', + }, + { + from: 'plugins/security_solution/common/api/detection_engine', + to: 'node_modules/lodash/sortBy.js', + }, + ], + }, + modules: [], + }, + }; + (cruise as jest.Mock).mockResolvedValue(customCruiseResult); + + const result = await identifyDependencyUsage([], 'lodash', { + groupBy: 'owner', + collapseDepth: 3, + summary: false, + }); + + expect(cruise).toHaveBeenCalled(); + expect(result).toEqual({ + team_security_analytics: { + modules: [ + 'plugins/security_solution/public/entity_analytics/components/componentA.ts', + 'plugins/security_solution/public/entity_analytics/components/componentB.ts', + 'plugins/security_solution/server/lib/analytics/analytics.ts', + ], + deps: ['lodash/fp.js', 'lodash/partition.js', 'lodash/cloneDeep.js'], + teams: ['team_security_analytics'], + }, + team_security_solution: { + modules: ['plugins/security_solution/common/api/detection_engine'], + deps: ['lodash/sortBy.js'], + teams: ['team_security_solution'], + }, + }); + }); +}); diff --git a/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.ts b/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.ts new file mode 100644 index 0000000000000..33817605cf11b --- /dev/null +++ b/packages/kbn-dependency-usage/src/dependency_graph/providers/cruiser.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import chalk from 'chalk'; +import { cruise } from 'dependency-cruiser'; +import fs from 'fs'; +import nodePath from 'path'; + +import { groupFilesByOwners } from '../../lib/group_by_owners.ts'; +import { groupBySource } from '../../lib/group_by_source.ts'; +import { createCollapseRegexWithDepth } from '../../lib/collapse_with_depth.ts'; +import { aggregationGroups, excludePaths } from '../common/constants.ts'; + +interface DependencyGraphOptions { + isVerbose?: boolean; + summary?: boolean; + collapseDepth: number; + groupBy?: string; +} + +type PathsToAnalyze = string[]; +type DependencyName = string | undefined; + +const invokeDependencyCruiser = async ( + paths: PathsToAnalyze, + dependencyName: DependencyName, + { summary, collapseDepth }: Omit +) => { + const collapseByNodeModule = !dependencyName || (dependencyName && summary); + const collapseByNodeModuleRegex = '^node_modules/(@[^/]+/[^/]+|[^/]+)'; + const collapseRules = [createCollapseRegexWithDepth(aggregationGroups, collapseDepth)]; + + if (collapseByNodeModule) { + collapseRules.push(collapseByNodeModuleRegex); + } + + const captureRule = dependencyName + ? { + name: `dependency-usage ${dependencyName}`, + severity: 'info', + from: { pathNot: '^node_modules' }, + to: { path: dependencyName }, + } + : { + name: 'external-deps', + severity: 'info', + from: { path: paths.map((path) => `^${path}`) }, + to: { path: '^node_modules' }, + }; + + const result = await cruise(paths, { + ruleSet: { + // @ts-ignore + forbidden: [captureRule], + }, + doNotFollow: { + path: 'node_modules', + }, + extensions: ['.ts', '.tsx'], + focus: '^node_modules', + exclude: { + path: excludePaths, + }, + onlyReachable: paths.map((path) => `^${path}`).join('|'), + includeOnly: ['^node_modules', ...paths.map((path) => `^${path}`)], + validate: true, + collapse: collapseRules.join('|'), + }); + + return result; +}; + +export async function identifyDependencyUsageWithCruiser( + paths: PathsToAnalyze, + dependencyName: string | undefined, + { groupBy, summary, collapseDepth, isVerbose }: DependencyGraphOptions +) { + const result = await invokeDependencyCruiser(paths, dependencyName, { + summary, + collapseDepth, + }); + + if (typeof result.output === 'string') { + throw new Error('Unexpected string output from cruise result'); + } + + console.log( + `${chalk.green(`Successfully`)} built dependency graph using ${chalk.bold.magenta( + 'cruiser' + )}. Analyzing...` + ); + + if (isVerbose) { + const verboseLogPath = nodePath.join(process.cwd(), '.dependency-graph-log.json'); + + fs.writeFile(verboseLogPath, JSON.stringify(result, null, 2), (err) => { + if (err) { + console.error( + chalk.red(`Failed to save dependency graph log to ${verboseLogPath}: ${err.message}`) + ); + } else { + console.log(chalk.yellow(`Dependency graph log saved to ${verboseLogPath}`)); + } + }); + } + + const { violations } = result.output.summary; + + if (groupBy === 'owner') { + return groupFilesByOwners(violations); + } + + if (dependencyName) { + const dependencyRegex = new RegExp(`node_modules/${dependencyName}`); + + const dependentsList = result.output.modules.reduce>( + (acc, { source, dependents }) => { + if (!dependencyRegex.test(source)) { + return acc; + } + + const transformedDependencyName = source.split('/')[1]; + if (!acc[transformedDependencyName]) { + acc[transformedDependencyName] = []; + } + + acc[transformedDependencyName].push(...dependents); + + return acc; + }, + {} + ); + + return { + modules: [...new Set(violations.map(({ from }) => from))], + ...(!summary && { dependents: dependentsList }), + }; + } + + return groupBySource(violations); +} diff --git a/packages/kbn-dependency-usage/src/lib/code_owners.test.ts b/packages/kbn-dependency-usage/src/lib/code_owners.test.ts new file mode 100644 index 0000000000000..e9c5c63ba2f98 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/code_owners.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getCodeOwnersForFile, PathWithOwners } from './code_owners'; + +describe('getCodeOwnersForFile', () => { + it('should return teams for exact file match', () => { + const reversedCodeowners = [ + { + path: 'src/file1.js', + teams: ['team_a'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath === 'src/file1.js' }), + }, + }, + ] as PathWithOwners[]; + + const result = getCodeOwnersForFile('src/file1.js', reversedCodeowners); + expect(result).toEqual(['team_a']); + }); + + it('should return "unknown" if no ownership is found', () => { + const reversedCodeowners = [ + { + path: 'src/file1.js', + teams: ['team_a'], + ignorePattern: { test: (filePath: string) => ({ ignored: filePath === 'src/file1.js' }) }, + }, + ] as PathWithOwners[]; + + const result = getCodeOwnersForFile('src/unknown_file.js', reversedCodeowners); + expect(result).toEqual(['unknown']); + }); + + it('should return teams for partial match if no exact match exists', () => { + const reversedCodeowners = [ + { + path: 'src/folder', + teams: ['team_c'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath.startsWith('src/folder') }), + }, + }, + ] as PathWithOwners[]; + + const result = getCodeOwnersForFile('src/folder/subfolder/file.js', reversedCodeowners); + expect(result).toEqual(['team_c']); + }); + + it('should handle root directory without ownership but with subdirectory owners', () => { + const reversedCodeowners = [ + { + path: 'folder/some/test', + teams: ['team_a'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath.startsWith('folder/some/test') }), + }, + }, + { + path: 'folder/another/test', + teams: ['team_b'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath.startsWith('folder/another/test') }), + }, + }, + ] as PathWithOwners[]; + + const result = getCodeOwnersForFile('folder', reversedCodeowners); + expect(result).toEqual(['team_a', 'team_b']); + }); + + it('should return all unique teams if multiple subdirectories match', () => { + const reversedCodeowners = [ + { + path: 'folder/some/test', + teams: ['team_a'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath.startsWith('folder/some/test') }), + }, + }, + { + path: 'folder/another/test', + teams: ['team_b'], + ignorePattern: { + test: (filePath: string) => ({ ignored: filePath.startsWith('folder/another/test') }), + }, + }, + ] as PathWithOwners[]; + + const result = getCodeOwnersForFile('folder/another/test/file.js', reversedCodeowners); + expect(result).toEqual(['team_b']); + }); +}); diff --git a/packages/kbn-dependency-usage/src/lib/code_owners.ts b/packages/kbn-dependency-usage/src/lib/code_owners.ts new file mode 100644 index 0000000000000..194dee7c80197 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/code_owners.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +// @ts-ignore +import { REPO_ROOT } from '@kbn/repo-info'; +import { join as joinPath } from 'path'; +import { existsSync, readFileSync } from 'fs'; + +import type { Ignore } from 'ignore'; +import ignore from 'ignore'; + +export interface PathWithOwners { + path: string; + teams: string[]; + ignorePattern: Ignore; +} + +const existOrThrow = (targetFile: string) => { + if (existsSync(targetFile) === false) + throw Error(`Unable to determine code owners: file ${targetFile} Not Found`); +}; + +/** + * Get the .github/CODEOWNERS entries, prepared for path matching. + * The last matching CODEOWNERS entry has highest precedence: + * https://help.github.com/articles/about-codeowners/ + * so entries are returned in reversed order to later search for the first match. + */ +export function getPathsWithOwnersReversed(): PathWithOwners[] { + const codeownersPath = joinPath(REPO_ROOT, '.github', 'CODEOWNERS'); + existOrThrow(codeownersPath); + const codeownersContent = readFileSync(codeownersPath, { encoding: 'utf8', flag: 'r' }); + const codeownersLines = codeownersContent.split(/\r?\n/); + const codeowners = codeownersLines + .map((line) => line.trim()) + .filter((line) => line && line[0] !== '#'); + + const pathsWithOwners: PathWithOwners[] = codeowners.map((c) => { + const [path, ...ghTeams] = c.split(/\s+/); + const cleanedPath = path.replace(/\/$/, ''); // remove trailing slash + const parsedTeams = ghTeams + .map((t) => t.replace('@', '').split(',')) + .flat() + .filter((t) => t.startsWith('elastic')); + return { + path: cleanedPath, + teams: parsedTeams, + // register CODEOWNERS entries with the `ignores` lib for later path matching + ignorePattern: ignore().add([cleanedPath]), + }; + }); + + return pathsWithOwners.reverse(); +} + +export function getCodeOwnersForFile( + filePath: string, + reversedCodeowners?: PathWithOwners[] +): string[] { + const pathsWithOwners = reversedCodeowners ?? getPathsWithOwnersReversed(); + + const match = pathsWithOwners.find((p) => p.ignorePattern.test(filePath).ignored); + + if (!match?.teams.length) { + const allTeams = pathsWithOwners + .filter((p) => p.path.includes(filePath) && p.teams.length) + .map((p) => p.teams) + .flat(); + + if (!allTeams.length) { + return ['unknown']; + } + + return [...new Set(allTeams)]; + } + + return match?.teams ?? []; +} diff --git a/packages/kbn-dependency-usage/src/lib/collapse_with_depth.test.ts b/packages/kbn-dependency-usage/src/lib/collapse_with_depth.test.ts new file mode 100644 index 0000000000000..f2f8c365fdd96 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/collapse_with_depth.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createCollapseRegexWithDepth } from './collapse_with_depth'; + +describe('createCollapseRegexWithDepth', () => { + it('should generate regex with a base path and depth of 0', () => { + const basePath = ['app/components']; + const depth = 0; + const regex = createCollapseRegexWithDepth(basePath, depth); + + expect(regex).toBe('^(app/components)'); + }); + + it('should generate regex with a base path and depth of 1', () => { + const basePath = ['src']; + const depth = 1; + const regex = createCollapseRegexWithDepth(basePath, depth); + + expect(regex).toBe('^(src)/([^/]+)'); + }); + + it('should generate regex with a base path and depth of 2', () => { + const basePath = ['src']; + const depth = 2; + const regex = createCollapseRegexWithDepth(basePath, depth); + + expect(regex).toBe('^(src)/([^/]+)/([^/]+)'); + }); +}); diff --git a/packages/kbn-dependency-usage/src/lib/collapse_with_depth.ts b/packages/kbn-dependency-usage/src/lib/collapse_with_depth.ts new file mode 100644 index 0000000000000..6f0c42df0dcfa --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/collapse_with_depth.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export function createCollapseRegexWithDepth(basePaths: string[], depth: number) { + let regex = `^(${basePaths.join('|')})`; + + for (let i = 0; i < depth; i++) { + regex += `/([^/]+)`; + } + + return regex; +} diff --git a/packages/kbn-dependency-usage/src/lib/group_by_owners.test.ts b/packages/kbn-dependency-usage/src/lib/group_by_owners.test.ts new file mode 100644 index 0000000000000..224db092db447 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/group_by_owners.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { groupFilesByOwners } from './group_by_owners'; + +jest.mock('./code_owners', () => ({ + getPathsWithOwnersReversed: jest.fn(), + getCodeOwnersForFile: jest.fn((file: string) => { + const owners: Record = { + '/src/file1.js': ['team_a'], + '/src/file2.js': ['team_b'], + '/src/file3.js': ['team_a', 'team_c'], + }; + return owners[file]; + }), +})); + +describe('groupFilesByOwners', () => { + it('should group files by single owners correctly', () => { + const dependencies = [ + { from: '/src/file1.js', to: 'node_modules/module1' }, + { from: '/src/file2.js', to: 'node_modules/module2' }, + ]; + + const result = groupFilesByOwners(dependencies); + + expect(result).toEqual({ + team_a: { + modules: ['/src/file1.js'], + deps: ['module1'], + teams: ['team_a'], + }, + team_b: { + modules: ['/src/file2.js'], + deps: ['module2'], + teams: ['team_b'], + }, + }); + }); + + it('should group files with multiple owners under "multiple_teams"', () => { + const dependencies = [ + { from: '/src/file3.js', to: 'node_modules/module3' }, + { from: '/src/file3.js', to: 'node_modules/module4' }, + ]; + + const result = groupFilesByOwners(dependencies); + + expect(result).toEqual({ + multiple_teams: [ + { + modules: ['/src/file3.js'], + deps: ['module3', 'module4'], + teams: ['team_a', 'team_c'], + }, + ], + }); + }); + + it('should handle files with unknown owners', () => { + const dependencies = [{ from: '/src/file_unknown.js', to: 'node_modules/module_unknown' }]; + + const result = groupFilesByOwners(dependencies); + + expect(result).toEqual({ + unknown: { + modules: ['/src/file_unknown.js'], + deps: ['module_unknown'], + teams: ['unknown'], + }, + }); + }); + + it('should correctly handle mixed ownership scenarios', () => { + const dependencies = [ + { from: '/src/file1.js', to: 'node_modules/module1' }, + { from: '/src/file2.js', to: 'node_modules/module2' }, + { from: '/src/file3.js', to: 'node_modules/module3' }, + { from: '/src/file3.js', to: 'node_modules/module4' }, + { from: '/src/file_unknown.js', to: 'node_modules/module_unknown' }, + ]; + + const result = groupFilesByOwners(dependencies); + + expect(result).toEqual({ + team_a: { + modules: ['/src/file1.js'], + deps: ['module1'], + teams: ['team_a'], + }, + team_b: { + modules: ['/src/file2.js'], + deps: ['module2'], + teams: ['team_b'], + }, + multiple_teams: [ + { + modules: ['/src/file3.js'], + deps: ['module3', 'module4'], + teams: ['team_a', 'team_c'], + }, + ], + unknown: { + modules: ['/src/file_unknown.js'], + deps: ['module_unknown'], + teams: ['unknown'], + }, + }); + }); +}); diff --git a/packages/kbn-dependency-usage/src/lib/group_by_owners.ts b/packages/kbn-dependency-usage/src/lib/group_by_owners.ts new file mode 100644 index 0000000000000..36a3a4ab5df74 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/group_by_owners.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getCodeOwnersForFile, getPathsWithOwnersReversed } from './code_owners.ts'; + +interface DependencyByOwnerEntry { + modules: T; + deps: T; + teams: T; +} + +const UNKNOWN_OWNER = 'unknown'; +const MULTIPLE_TEAMS_OWNER = 'multiple_teams'; + +export function groupFilesByOwners(dependencies: Array<{ from: string; to: string }>) { + const ownerFilesMap = new Map(); + const reversedCodeowners = getPathsWithOwnersReversed(); + + for (const dep of dependencies) { + const { from, to } = dep; + + const owners = getCodeOwnersForFile(from, reversedCodeowners) ?? [UNKNOWN_OWNER]; + const ownerKey = owners.length > 1 ? MULTIPLE_TEAMS_OWNER : owners[0]; + + if (ownerKey === MULTIPLE_TEAMS_OWNER) { + if (!ownerFilesMap.has(ownerKey)) { + ownerFilesMap.set(ownerKey, new Map()); + } + + const modulesMap = ownerFilesMap.get(ownerKey); + + if (!modulesMap.has(from)) { + modulesMap.set(from, { deps: new Set(), modules: new Set(), teams: new Set() }); + } + + const moduleEntry = modulesMap.get(from); + + moduleEntry.deps.add(to.replace(/^node_modules\//, '')); + moduleEntry.modules.add(from); + + for (const owner of owners) { + moduleEntry.teams.add(owner); + } + + continue; + } + + if (!ownerFilesMap.has(ownerKey)) { + ownerFilesMap.set(ownerKey, { deps: new Set(), modules: new Set(), teams: new Set(owners) }); + } + + ownerFilesMap.get(ownerKey).deps.add(to.replace(/^node_modules\//, '')); + ownerFilesMap.get(ownerKey).modules.add(from); + } + + const result: Record = {}; + + const transformRecord = (entry: DependencyByOwnerEntry>) => ({ + modules: Array.from(entry.modules), + deps: Array.from(entry.deps), + teams: Array.from(entry.teams), + }); + + for (const [key, ownerRecord] of ownerFilesMap.entries()) { + const isMultiTeamRecord = key === MULTIPLE_TEAMS_OWNER; + + if (isMultiTeamRecord) { + if (!Array.isArray(result[MULTIPLE_TEAMS_OWNER])) { + result[MULTIPLE_TEAMS_OWNER] = []; + } + + for (const [, multiTeamRecord] of ownerRecord.entries()) { + (result[key] as DependencyByOwnerEntry[]).push(transformRecord(multiTeamRecord)); + } + + continue; + } + + result[key] = transformRecord(ownerRecord); + } + + return result; +} diff --git a/packages/kbn-dependency-usage/src/lib/group_by_source.test.ts b/packages/kbn-dependency-usage/src/lib/group_by_source.test.ts new file mode 100644 index 0000000000000..1ebce6936c1db --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/group_by_source.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { groupBySource } from './group_by_source.ts'; + +describe('groupBySource', () => { + it('should group dependencies by their source files', () => { + const dependencies = [ + { from: 'src/file1.js', to: 'node_modules/module1' }, + { from: 'src/file1.js', to: 'node_modules/module2' }, + { from: 'src/file2.js', to: 'node_modules/module3' }, + ]; + + const result = groupBySource(dependencies); + + expect(result).toEqual({ + 'src/file1.js': ['module1', 'module2'], + 'src/file2.js': ['module3'], + }); + }); + + it('should handle a single dependency', () => { + const dependencies = [{ from: 'src/file1.js', to: 'node_modules/module1' }]; + + const result = groupBySource(dependencies); + + expect(result).toEqual({ + 'src/file1.js': ['module1'], + }); + }); + + it('should handle multiple dependencies from the same source', () => { + const dependencies = [ + { from: 'src/file1.js', to: 'node_modules/module1' }, + { from: 'src/file1.js', to: 'node_modules/module2' }, + { from: 'src/file1.js', to: 'node_modules/module3' }, + ]; + + const result = groupBySource(dependencies); + + expect(result).toEqual({ + 'src/file1.js': ['module1', 'module2', 'module3'], + }); + }); + + it('should handle dependencies from different sources', () => { + const dependencies = [ + { from: 'src/file1.js', to: 'node_modules/module1' }, + { from: 'src/file2.js', to: 'node_modules/module2' }, + { from: 'src/file3.js', to: 'node_modules/module3' }, + ]; + + const result = groupBySource(dependencies); + + expect(result).toEqual({ + 'src/file1.js': ['module1'], + 'src/file2.js': ['module2'], + 'src/file3.js': ['module3'], + }); + }); + + it('should remove "node_modules/" prefix from dependencies', () => { + const dependencies = [ + { from: 'src/file1.js', to: 'node_modules/module1' }, + { from: 'src/file1.js', to: 'node_modules/module2' }, + ]; + + const result = groupBySource(dependencies); + + expect(result).toEqual({ + 'src/file1.js': ['module1', 'module2'], + }); + }); + + it('should return an empty object if there are no dependencies', () => { + const result = groupBySource([]); + + expect(result).toEqual({}); + }); +}); diff --git a/packages/kbn-dependency-usage/src/lib/group_by_source.ts b/packages/kbn-dependency-usage/src/lib/group_by_source.ts new file mode 100644 index 0000000000000..9275e6fd8392e --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/group_by_source.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export function groupBySource(dependencies: Array<{ from: string; to: string }>) { + const packageMap = new Map(); + + for (const dep of dependencies) { + const { from, to } = dep; + + if (!packageMap.has(from)) { + packageMap.set(from, new Set()); + } + + packageMap.get(from).add(to.replace(/^node_modules\//, '')); + } + + const result: Record = {}; + + for (const [key, value] of packageMap.entries()) { + result[key] = Array.from(value); + } + + return result; +} diff --git a/packages/kbn-dependency-usage/src/lib/index.ts b/packages/kbn-dependency-usage/src/lib/index.ts new file mode 100644 index 0000000000000..ddf97e41d0bd5 --- /dev/null +++ b/packages/kbn-dependency-usage/src/lib/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { groupFilesByOwners } from './group_by_owners.ts'; +export { groupBySource } from './group_by_source.ts'; diff --git a/packages/kbn-dependency-usage/tsconfig.json b/packages/kbn-dependency-usage/tsconfig.json new file mode 100644 index 0000000000000..96b87da389c39 --- /dev/null +++ b/packages/kbn-dependency-usage/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node", + "outDir": "target/types", + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "noEmit": true, + "allowImportingTsExtensions": true, + }, + "include": ["**/*.ts"], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/repo-info", + ], +} diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index a31e1f1641e8b..44bd57ed8f3d1 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -1001,5 +1001,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D context: `${KIBANA_DOCS}playground-context.html`, hiddenFields: `${KIBANA_DOCS}playground-query.html#playground-hidden-fields`, }, + inferenceManagement: { + inferenceAPIDocumentation: `${ELASTIC_WEBSITE_URL}docs/api/doc/elasticsearch/operation/operation-inference-put`, + }, }); }; diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ac0f66d83b705..a344d2d694c05 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -676,6 +676,9 @@ export interface DocLinks { readonly context: string; readonly hiddenFields: string; }; + readonly inferenceManagement: { + readonly inferenceAPIDocumentation: string; + }; } export type BuildFlavor = 'serverless' | 'traditional'; diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index 87f9dd15517c9..d3675e04c2663 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -695,5 +695,5 @@ export interface ESQLSearchParams { locale?: string; include_ccs_metadata?: boolean; dropNullColumns?: boolean; - params?: Array>; + params?: estypesWithoutBodyKey.ScalarValue[] | Array>; } diff --git a/packages/kbn-esql-ast/src/antlr/esql_lexer.g4 b/packages/kbn-esql-ast/src/antlr/esql_lexer.g4 index ad17de2984ad7..28ba50ef3efef 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_lexer.g4 +++ b/packages/kbn-esql-ast/src/antlr/esql_lexer.g4 @@ -87,8 +87,15 @@ WHERE : 'where' -> pushMode(EXPRESSION_MODE); // main section while preserving alphabetical order: // MYCOMMAND : 'mycommand' -> ... DEV_INLINESTATS : {this.isDevVersion()}? 'inlinestats' -> pushMode(EXPRESSION_MODE); -DEV_LOOKUP : {this.isDevVersion()}? 'lookup' -> pushMode(LOOKUP_MODE); +DEV_LOOKUP : {this.isDevVersion()}? 'lookup_🐔' -> pushMode(LOOKUP_MODE); DEV_METRICS : {this.isDevVersion()}? 'metrics' -> pushMode(METRICS_MODE); +// list of all JOIN commands +DEV_JOIN : {this.isDevVersion()}? 'join' -> pushMode(JOIN_MODE); +DEV_JOIN_FULL : {this.isDevVersion()}? 'full' -> pushMode(JOIN_MODE); +DEV_JOIN_LEFT : {this.isDevVersion()}? 'left' -> pushMode(JOIN_MODE); +DEV_JOIN_RIGHT : {this.isDevVersion()}? 'right' -> pushMode(JOIN_MODE); +DEV_JOIN_LOOKUP : {this.isDevVersion()}? 'lookup' -> pushMode(JOIN_MODE); + // // Catch-all for unrecognized commands - don't define any beyond this line @@ -107,8 +114,6 @@ WS : [ \r\n\t]+ -> channel(HIDDEN) ; -COLON : ':'; - // // Expression - used by most command // @@ -179,6 +184,7 @@ AND : 'and'; ASC : 'asc'; ASSIGN : '='; CAST_OP : '::'; +COLON : ':'; COMMA : ','; DESC : 'desc'; DOT : '.'; @@ -211,7 +217,6 @@ MINUS : '-'; ASTERISK : '*'; SLASH : '/'; PERCENT : '%'; -EXPRESSION_COLON : {this.isDevVersion()}? COLON -> type(COLON); NESTED_WHERE : WHERE -> type(WHERE); @@ -545,6 +550,31 @@ LOOKUP_FIELD_WS : WS -> channel(HIDDEN) ; +// +// JOIN-related commands +// +mode JOIN_MODE; +JOIN_PIPE : PIPE -> type(PIPE), popMode; +JOIN_JOIN : DEV_JOIN -> type(DEV_JOIN); +JOIN_AS : AS -> type(AS); +JOIN_ON : ON -> type(ON), popMode, pushMode(EXPRESSION_MODE); +USING : 'USING' -> popMode, pushMode(EXPRESSION_MODE); + +JOIN_UNQUOTED_IDENTIFER: UNQUOTED_IDENTIFIER -> type(UNQUOTED_IDENTIFIER); +JOIN_QUOTED_IDENTIFIER : QUOTED_IDENTIFIER -> type(QUOTED_IDENTIFIER); + +JOIN_LINE_COMMENT + : LINE_COMMENT -> channel(HIDDEN) + ; + +JOIN_MULTILINE_COMMENT + : MULTILINE_COMMENT -> channel(HIDDEN) + ; + +JOIN_WS + : WS -> channel(HIDDEN) + ; + // // METRICS command // diff --git a/packages/kbn-esql-ast/src/antlr/esql_lexer.interp b/packages/kbn-esql-ast/src/antlr/esql_lexer.interp index 8f9c5956dddd5..c83fdbe8847a9 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_lexer.interp +++ b/packages/kbn-esql-ast/src/antlr/esql_lexer.interp @@ -23,7 +23,11 @@ null null null null -':' +null +null +null +null +null '|' null null @@ -33,6 +37,7 @@ null 'asc' '=' '::' +':' ',' 'desc' '.' @@ -113,6 +118,10 @@ null null null null +'USING' +null +null +null null null null @@ -141,11 +150,15 @@ WHERE DEV_INLINESTATS DEV_LOOKUP DEV_METRICS +DEV_JOIN +DEV_JOIN_FULL +DEV_JOIN_LEFT +DEV_JOIN_RIGHT +DEV_JOIN_LOOKUP UNKNOWN_CMD LINE_COMMENT MULTILINE_COMMENT WS -COLON PIPE QUOTED_STRING INTEGER_LITERAL @@ -155,6 +168,7 @@ AND ASC ASSIGN CAST_OP +COLON COMMA DESC DOT @@ -235,6 +249,10 @@ LOOKUP_WS LOOKUP_FIELD_LINE_COMMENT LOOKUP_FIELD_MULTILINE_COMMENT LOOKUP_FIELD_WS +USING +JOIN_LINE_COMMENT +JOIN_MULTILINE_COMMENT +JOIN_WS METRICS_LINE_COMMENT METRICS_MULTILINE_COMMENT METRICS_WS @@ -262,11 +280,15 @@ WHERE DEV_INLINESTATS DEV_LOOKUP DEV_METRICS +DEV_JOIN +DEV_JOIN_FULL +DEV_JOIN_LEFT +DEV_JOIN_RIGHT +DEV_JOIN_LOOKUP UNKNOWN_CMD LINE_COMMENT MULTILINE_COMMENT WS -COLON PIPE DIGIT LETTER @@ -286,6 +308,7 @@ AND ASC ASSIGN CAST_OP +COLON COMMA DESC DOT @@ -316,7 +339,6 @@ MINUS ASTERISK SLASH PERCENT -EXPRESSION_COLON NESTED_WHERE NAMED_OR_POSITIONAL_PARAM OPENING_BRACKET @@ -427,6 +449,16 @@ LOOKUP_FIELD_ID_PATTERN LOOKUP_FIELD_LINE_COMMENT LOOKUP_FIELD_MULTILINE_COMMENT LOOKUP_FIELD_WS +JOIN_PIPE +JOIN_JOIN +JOIN_AS +JOIN_ON +USING +JOIN_UNQUOTED_IDENTIFER +JOIN_QUOTED_IDENTIFIER +JOIN_LINE_COMMENT +JOIN_MULTILINE_COMMENT +JOIN_WS METRICS_PIPE METRICS_UNQUOTED_SOURCE METRICS_QUOTED_SOURCE @@ -461,8 +493,9 @@ SHOW_MODE SETTING_MODE LOOKUP_MODE LOOKUP_FIELD_MODE +JOIN_MODE METRICS_MODE CLOSING_METRICS_MODE atn: -[4, 0, 119, 1484, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 2, 175, 7, 175, 2, 176, 7, 176, 2, 177, 7, 177, 2, 178, 7, 178, 2, 179, 7, 179, 2, 180, 7, 180, 2, 181, 7, 181, 2, 182, 7, 182, 2, 183, 7, 183, 2, 184, 7, 184, 2, 185, 7, 185, 2, 186, 7, 186, 2, 187, 7, 187, 2, 188, 7, 188, 2, 189, 7, 189, 2, 190, 7, 190, 2, 191, 7, 191, 2, 192, 7, 192, 2, 193, 7, 193, 2, 194, 7, 194, 2, 195, 7, 195, 2, 196, 7, 196, 2, 197, 7, 197, 2, 198, 7, 198, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 4, 19, 580, 8, 19, 11, 19, 12, 19, 581, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 5, 20, 590, 8, 20, 10, 20, 12, 20, 593, 9, 20, 1, 20, 3, 20, 596, 8, 20, 1, 20, 3, 20, 599, 8, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 5, 21, 608, 8, 21, 10, 21, 12, 21, 611, 9, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 4, 22, 619, 8, 22, 11, 22, 12, 22, 620, 1, 22, 1, 22, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 29, 1, 29, 3, 29, 642, 8, 29, 1, 29, 4, 29, 645, 8, 29, 11, 29, 12, 29, 646, 1, 30, 1, 30, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 3, 32, 656, 8, 32, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 663, 8, 34, 1, 35, 1, 35, 1, 35, 5, 35, 668, 8, 35, 10, 35, 12, 35, 671, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 679, 8, 35, 10, 35, 12, 35, 682, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 3, 35, 689, 8, 35, 1, 35, 3, 35, 692, 8, 35, 3, 35, 694, 8, 35, 1, 36, 4, 36, 697, 8, 36, 11, 36, 12, 36, 698, 1, 37, 4, 37, 702, 8, 37, 11, 37, 12, 37, 703, 1, 37, 1, 37, 5, 37, 708, 8, 37, 10, 37, 12, 37, 711, 9, 37, 1, 37, 1, 37, 4, 37, 715, 8, 37, 11, 37, 12, 37, 716, 1, 37, 4, 37, 720, 8, 37, 11, 37, 12, 37, 721, 1, 37, 1, 37, 5, 37, 726, 8, 37, 10, 37, 12, 37, 729, 9, 37, 3, 37, 731, 8, 37, 1, 37, 1, 37, 1, 37, 1, 37, 4, 37, 737, 8, 37, 11, 37, 12, 37, 738, 1, 37, 1, 37, 3, 37, 743, 8, 37, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 69, 1, 69, 1, 70, 1, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 3, 75, 874, 8, 75, 1, 75, 5, 75, 877, 8, 75, 10, 75, 12, 75, 880, 9, 75, 1, 75, 1, 75, 4, 75, 884, 8, 75, 11, 75, 12, 75, 885, 3, 75, 888, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 5, 78, 902, 8, 78, 10, 78, 12, 78, 905, 9, 78, 1, 78, 1, 78, 3, 78, 909, 8, 78, 1, 78, 4, 78, 912, 8, 78, 11, 78, 12, 78, 913, 3, 78, 916, 8, 78, 1, 79, 1, 79, 4, 79, 920, 8, 79, 11, 79, 12, 79, 921, 1, 79, 1, 79, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 83, 1, 83, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 1, 88, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 1, 90, 1, 91, 1, 91, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 1, 92, 1, 93, 1, 93, 1, 93, 1, 93, 1, 94, 1, 94, 1, 94, 1, 94, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 96, 1, 96, 1, 96, 3, 96, 999, 8, 96, 1, 97, 4, 97, 1002, 8, 97, 11, 97, 12, 97, 1003, 1, 98, 1, 98, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 1, 100, 1, 101, 1, 101, 1, 101, 1, 101, 1, 102, 1, 102, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 108, 3, 108, 1053, 8, 108, 1, 109, 1, 109, 3, 109, 1057, 8, 109, 1, 109, 5, 109, 1060, 8, 109, 10, 109, 12, 109, 1063, 9, 109, 1, 109, 1, 109, 3, 109, 1067, 8, 109, 1, 109, 4, 109, 1070, 8, 109, 11, 109, 12, 109, 1071, 3, 109, 1074, 8, 109, 1, 110, 1, 110, 4, 110, 1078, 8, 110, 11, 110, 12, 110, 1079, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 1, 113, 1, 113, 1, 113, 1, 113, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 115, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 129, 1, 129, 1, 130, 4, 130, 1165, 8, 130, 11, 130, 12, 130, 1166, 1, 130, 1, 130, 3, 130, 1171, 8, 130, 1, 130, 4, 130, 1174, 8, 130, 11, 130, 12, 130, 1175, 1, 131, 1, 131, 1, 131, 1, 131, 1, 132, 1, 132, 1, 132, 1, 132, 1, 133, 1, 133, 1, 133, 1, 133, 1, 134, 1, 134, 1, 134, 1, 134, 1, 135, 1, 135, 1, 135, 1, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 1, 136, 1, 137, 1, 137, 1, 137, 1, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 140, 1, 140, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 1, 141, 1, 142, 1, 142, 1, 142, 1, 142, 1, 142, 1, 143, 1, 143, 1, 143, 1, 143, 1, 143, 1, 144, 1, 144, 1, 144, 1, 144, 1, 145, 1, 145, 1, 145, 1, 145, 1, 146, 1, 146, 1, 146, 1, 146, 1, 147, 1, 147, 1, 147, 1, 147, 1, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 1, 150, 1, 150, 1, 151, 1, 151, 1, 151, 1, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 158, 1, 158, 1, 158, 1, 158, 1, 159, 1, 159, 1, 159, 1, 159, 1, 160, 1, 160, 1, 160, 1, 160, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 1, 162, 1, 162, 1, 163, 1, 163, 1, 163, 1, 163, 1, 163, 4, 163, 1321, 8, 163, 11, 163, 12, 163, 1322, 1, 164, 1, 164, 1, 164, 1, 164, 1, 165, 1, 165, 1, 165, 1, 165, 1, 166, 1, 166, 1, 166, 1, 166, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 168, 1, 168, 1, 168, 1, 168, 1, 169, 1, 169, 1, 169, 1, 169, 1, 170, 1, 170, 1, 170, 1, 170, 1, 171, 1, 171, 1, 171, 1, 171, 1, 171, 1, 172, 1, 172, 1, 172, 1, 172, 1, 173, 1, 173, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 1, 174, 1, 175, 1, 175, 1, 175, 1, 175, 1, 176, 1, 176, 1, 176, 1, 176, 1, 177, 1, 177, 1, 177, 1, 177, 1, 177, 1, 177, 1, 178, 1, 178, 1, 178, 1, 178, 1, 179, 1, 179, 1, 179, 1, 179, 1, 180, 1, 180, 1, 180, 1, 180, 1, 181, 1, 181, 1, 181, 1, 181, 1, 182, 1, 182, 1, 182, 1, 182, 1, 183, 1, 183, 1, 183, 1, 183, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 185, 1, 185, 1, 185, 1, 185, 1, 185, 1, 185, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 187, 1, 187, 1, 187, 1, 187, 1, 188, 1, 188, 1, 188, 1, 188, 1, 189, 1, 189, 1, 189, 1, 189, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 192, 1, 192, 1, 192, 1, 192, 1, 193, 1, 193, 1, 193, 1, 193, 1, 194, 1, 194, 1, 194, 1, 194, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 2, 609, 680, 0, 199, 15, 1, 17, 2, 19, 3, 21, 4, 23, 5, 25, 6, 27, 7, 29, 8, 31, 9, 33, 10, 35, 11, 37, 12, 39, 13, 41, 14, 43, 15, 45, 16, 47, 17, 49, 18, 51, 19, 53, 20, 55, 21, 57, 22, 59, 23, 61, 24, 63, 25, 65, 0, 67, 0, 69, 0, 71, 0, 73, 0, 75, 0, 77, 0, 79, 0, 81, 0, 83, 0, 85, 26, 87, 27, 89, 28, 91, 29, 93, 30, 95, 31, 97, 32, 99, 33, 101, 34, 103, 35, 105, 36, 107, 37, 109, 38, 111, 39, 113, 40, 115, 41, 117, 42, 119, 43, 121, 44, 123, 45, 125, 46, 127, 47, 129, 48, 131, 49, 133, 50, 135, 51, 137, 52, 139, 53, 141, 54, 143, 55, 145, 56, 147, 57, 149, 58, 151, 59, 153, 60, 155, 61, 157, 62, 159, 63, 161, 0, 163, 0, 165, 64, 167, 65, 169, 66, 171, 67, 173, 0, 175, 68, 177, 69, 179, 70, 181, 71, 183, 0, 185, 0, 187, 72, 189, 73, 191, 74, 193, 0, 195, 0, 197, 0, 199, 0, 201, 0, 203, 0, 205, 75, 207, 0, 209, 76, 211, 0, 213, 0, 215, 77, 217, 78, 219, 79, 221, 0, 223, 0, 225, 0, 227, 0, 229, 0, 231, 0, 233, 0, 235, 80, 237, 81, 239, 82, 241, 83, 243, 0, 245, 0, 247, 0, 249, 0, 251, 0, 253, 0, 255, 84, 257, 0, 259, 85, 261, 86, 263, 87, 265, 0, 267, 0, 269, 88, 271, 89, 273, 0, 275, 90, 277, 0, 279, 91, 281, 92, 283, 93, 285, 0, 287, 0, 289, 0, 291, 0, 293, 0, 295, 0, 297, 0, 299, 0, 301, 0, 303, 94, 305, 95, 307, 96, 309, 0, 311, 0, 313, 0, 315, 0, 317, 0, 319, 0, 321, 97, 323, 98, 325, 99, 327, 0, 329, 100, 331, 101, 333, 102, 335, 103, 337, 0, 339, 0, 341, 104, 343, 105, 345, 106, 347, 107, 349, 0, 351, 0, 353, 0, 355, 0, 357, 0, 359, 0, 361, 0, 363, 108, 365, 109, 367, 110, 369, 0, 371, 0, 373, 0, 375, 0, 377, 111, 379, 112, 381, 113, 383, 0, 385, 0, 387, 0, 389, 114, 391, 115, 393, 116, 395, 0, 397, 0, 399, 117, 401, 118, 403, 119, 405, 0, 407, 0, 409, 0, 411, 0, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 35, 2, 0, 68, 68, 100, 100, 2, 0, 73, 73, 105, 105, 2, 0, 83, 83, 115, 115, 2, 0, 69, 69, 101, 101, 2, 0, 67, 67, 99, 99, 2, 0, 84, 84, 116, 116, 2, 0, 82, 82, 114, 114, 2, 0, 79, 79, 111, 111, 2, 0, 80, 80, 112, 112, 2, 0, 78, 78, 110, 110, 2, 0, 72, 72, 104, 104, 2, 0, 86, 86, 118, 118, 2, 0, 65, 65, 97, 97, 2, 0, 76, 76, 108, 108, 2, 0, 88, 88, 120, 120, 2, 0, 70, 70, 102, 102, 2, 0, 77, 77, 109, 109, 2, 0, 71, 71, 103, 103, 2, 0, 75, 75, 107, 107, 2, 0, 87, 87, 119, 119, 2, 0, 85, 85, 117, 117, 6, 0, 9, 10, 13, 13, 32, 32, 47, 47, 91, 91, 93, 93, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 2, 0, 65, 90, 97, 122, 8, 0, 34, 34, 78, 78, 82, 82, 84, 84, 92, 92, 110, 110, 114, 114, 116, 116, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 43, 43, 45, 45, 1, 0, 96, 96, 2, 0, 66, 66, 98, 98, 2, 0, 89, 89, 121, 121, 11, 0, 9, 10, 13, 13, 32, 32, 34, 34, 44, 44, 47, 47, 58, 58, 61, 61, 91, 91, 93, 93, 124, 124, 2, 0, 42, 42, 47, 47, 11, 0, 9, 10, 13, 13, 32, 32, 34, 35, 44, 44, 47, 47, 58, 58, 60, 60, 62, 63, 92, 92, 124, 124, 1512, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 1, 63, 1, 0, 0, 0, 1, 85, 1, 0, 0, 0, 1, 87, 1, 0, 0, 0, 1, 89, 1, 0, 0, 0, 1, 91, 1, 0, 0, 0, 1, 93, 1, 0, 0, 0, 1, 95, 1, 0, 0, 0, 1, 97, 1, 0, 0, 0, 1, 99, 1, 0, 0, 0, 1, 101, 1, 0, 0, 0, 1, 103, 1, 0, 0, 0, 1, 105, 1, 0, 0, 0, 1, 107, 1, 0, 0, 0, 1, 109, 1, 0, 0, 0, 1, 111, 1, 0, 0, 0, 1, 113, 1, 0, 0, 0, 1, 115, 1, 0, 0, 0, 1, 117, 1, 0, 0, 0, 1, 119, 1, 0, 0, 0, 1, 121, 1, 0, 0, 0, 1, 123, 1, 0, 0, 0, 1, 125, 1, 0, 0, 0, 1, 127, 1, 0, 0, 0, 1, 129, 1, 0, 0, 0, 1, 131, 1, 0, 0, 0, 1, 133, 1, 0, 0, 0, 1, 135, 1, 0, 0, 0, 1, 137, 1, 0, 0, 0, 1, 139, 1, 0, 0, 0, 1, 141, 1, 0, 0, 0, 1, 143, 1, 0, 0, 0, 1, 145, 1, 0, 0, 0, 1, 147, 1, 0, 0, 0, 1, 149, 1, 0, 0, 0, 1, 151, 1, 0, 0, 0, 1, 153, 1, 0, 0, 0, 1, 155, 1, 0, 0, 0, 1, 157, 1, 0, 0, 0, 1, 159, 1, 0, 0, 0, 1, 161, 1, 0, 0, 0, 1, 163, 1, 0, 0, 0, 1, 165, 1, 0, 0, 0, 1, 167, 1, 0, 0, 0, 1, 169, 1, 0, 0, 0, 1, 171, 1, 0, 0, 0, 1, 175, 1, 0, 0, 0, 1, 177, 1, 0, 0, 0, 1, 179, 1, 0, 0, 0, 1, 181, 1, 0, 0, 0, 2, 183, 1, 0, 0, 0, 2, 185, 1, 0, 0, 0, 2, 187, 1, 0, 0, 0, 2, 189, 1, 0, 0, 0, 2, 191, 1, 0, 0, 0, 3, 193, 1, 0, 0, 0, 3, 195, 1, 0, 0, 0, 3, 197, 1, 0, 0, 0, 3, 199, 1, 0, 0, 0, 3, 201, 1, 0, 0, 0, 3, 203, 1, 0, 0, 0, 3, 205, 1, 0, 0, 0, 3, 209, 1, 0, 0, 0, 3, 211, 1, 0, 0, 0, 3, 213, 1, 0, 0, 0, 3, 215, 1, 0, 0, 0, 3, 217, 1, 0, 0, 0, 3, 219, 1, 0, 0, 0, 4, 221, 1, 0, 0, 0, 4, 223, 1, 0, 0, 0, 4, 225, 1, 0, 0, 0, 4, 227, 1, 0, 0, 0, 4, 229, 1, 0, 0, 0, 4, 235, 1, 0, 0, 0, 4, 237, 1, 0, 0, 0, 4, 239, 1, 0, 0, 0, 4, 241, 1, 0, 0, 0, 5, 243, 1, 0, 0, 0, 5, 245, 1, 0, 0, 0, 5, 247, 1, 0, 0, 0, 5, 249, 1, 0, 0, 0, 5, 251, 1, 0, 0, 0, 5, 253, 1, 0, 0, 0, 5, 255, 1, 0, 0, 0, 5, 257, 1, 0, 0, 0, 5, 259, 1, 0, 0, 0, 5, 261, 1, 0, 0, 0, 5, 263, 1, 0, 0, 0, 6, 265, 1, 0, 0, 0, 6, 267, 1, 0, 0, 0, 6, 269, 1, 0, 0, 0, 6, 271, 1, 0, 0, 0, 6, 275, 1, 0, 0, 0, 6, 277, 1, 0, 0, 0, 6, 279, 1, 0, 0, 0, 6, 281, 1, 0, 0, 0, 6, 283, 1, 0, 0, 0, 7, 285, 1, 0, 0, 0, 7, 287, 1, 0, 0, 0, 7, 289, 1, 0, 0, 0, 7, 291, 1, 0, 0, 0, 7, 293, 1, 0, 0, 0, 7, 295, 1, 0, 0, 0, 7, 297, 1, 0, 0, 0, 7, 299, 1, 0, 0, 0, 7, 301, 1, 0, 0, 0, 7, 303, 1, 0, 0, 0, 7, 305, 1, 0, 0, 0, 7, 307, 1, 0, 0, 0, 8, 309, 1, 0, 0, 0, 8, 311, 1, 0, 0, 0, 8, 313, 1, 0, 0, 0, 8, 315, 1, 0, 0, 0, 8, 317, 1, 0, 0, 0, 8, 319, 1, 0, 0, 0, 8, 321, 1, 0, 0, 0, 8, 323, 1, 0, 0, 0, 8, 325, 1, 0, 0, 0, 9, 327, 1, 0, 0, 0, 9, 329, 1, 0, 0, 0, 9, 331, 1, 0, 0, 0, 9, 333, 1, 0, 0, 0, 9, 335, 1, 0, 0, 0, 10, 337, 1, 0, 0, 0, 10, 339, 1, 0, 0, 0, 10, 341, 1, 0, 0, 0, 10, 343, 1, 0, 0, 0, 10, 345, 1, 0, 0, 0, 10, 347, 1, 0, 0, 0, 11, 349, 1, 0, 0, 0, 11, 351, 1, 0, 0, 0, 11, 353, 1, 0, 0, 0, 11, 355, 1, 0, 0, 0, 11, 357, 1, 0, 0, 0, 11, 359, 1, 0, 0, 0, 11, 361, 1, 0, 0, 0, 11, 363, 1, 0, 0, 0, 11, 365, 1, 0, 0, 0, 11, 367, 1, 0, 0, 0, 12, 369, 1, 0, 0, 0, 12, 371, 1, 0, 0, 0, 12, 373, 1, 0, 0, 0, 12, 375, 1, 0, 0, 0, 12, 377, 1, 0, 0, 0, 12, 379, 1, 0, 0, 0, 12, 381, 1, 0, 0, 0, 13, 383, 1, 0, 0, 0, 13, 385, 1, 0, 0, 0, 13, 387, 1, 0, 0, 0, 13, 389, 1, 0, 0, 0, 13, 391, 1, 0, 0, 0, 13, 393, 1, 0, 0, 0, 14, 395, 1, 0, 0, 0, 14, 397, 1, 0, 0, 0, 14, 399, 1, 0, 0, 0, 14, 401, 1, 0, 0, 0, 14, 403, 1, 0, 0, 0, 14, 405, 1, 0, 0, 0, 14, 407, 1, 0, 0, 0, 14, 409, 1, 0, 0, 0, 14, 411, 1, 0, 0, 0, 15, 413, 1, 0, 0, 0, 17, 423, 1, 0, 0, 0, 19, 430, 1, 0, 0, 0, 21, 439, 1, 0, 0, 0, 23, 446, 1, 0, 0, 0, 25, 456, 1, 0, 0, 0, 27, 463, 1, 0, 0, 0, 29, 470, 1, 0, 0, 0, 31, 477, 1, 0, 0, 0, 33, 485, 1, 0, 0, 0, 35, 497, 1, 0, 0, 0, 37, 506, 1, 0, 0, 0, 39, 512, 1, 0, 0, 0, 41, 519, 1, 0, 0, 0, 43, 526, 1, 0, 0, 0, 45, 534, 1, 0, 0, 0, 47, 542, 1, 0, 0, 0, 49, 557, 1, 0, 0, 0, 51, 567, 1, 0, 0, 0, 53, 579, 1, 0, 0, 0, 55, 585, 1, 0, 0, 0, 57, 602, 1, 0, 0, 0, 59, 618, 1, 0, 0, 0, 61, 624, 1, 0, 0, 0, 63, 626, 1, 0, 0, 0, 65, 630, 1, 0, 0, 0, 67, 632, 1, 0, 0, 0, 69, 634, 1, 0, 0, 0, 71, 637, 1, 0, 0, 0, 73, 639, 1, 0, 0, 0, 75, 648, 1, 0, 0, 0, 77, 650, 1, 0, 0, 0, 79, 655, 1, 0, 0, 0, 81, 657, 1, 0, 0, 0, 83, 662, 1, 0, 0, 0, 85, 693, 1, 0, 0, 0, 87, 696, 1, 0, 0, 0, 89, 742, 1, 0, 0, 0, 91, 744, 1, 0, 0, 0, 93, 747, 1, 0, 0, 0, 95, 751, 1, 0, 0, 0, 97, 755, 1, 0, 0, 0, 99, 757, 1, 0, 0, 0, 101, 760, 1, 0, 0, 0, 103, 762, 1, 0, 0, 0, 105, 767, 1, 0, 0, 0, 107, 769, 1, 0, 0, 0, 109, 775, 1, 0, 0, 0, 111, 781, 1, 0, 0, 0, 113, 784, 1, 0, 0, 0, 115, 787, 1, 0, 0, 0, 117, 792, 1, 0, 0, 0, 119, 797, 1, 0, 0, 0, 121, 799, 1, 0, 0, 0, 123, 803, 1, 0, 0, 0, 125, 808, 1, 0, 0, 0, 127, 814, 1, 0, 0, 0, 129, 817, 1, 0, 0, 0, 131, 819, 1, 0, 0, 0, 133, 825, 1, 0, 0, 0, 135, 827, 1, 0, 0, 0, 137, 832, 1, 0, 0, 0, 139, 835, 1, 0, 0, 0, 141, 838, 1, 0, 0, 0, 143, 841, 1, 0, 0, 0, 145, 843, 1, 0, 0, 0, 147, 846, 1, 0, 0, 0, 149, 848, 1, 0, 0, 0, 151, 851, 1, 0, 0, 0, 153, 853, 1, 0, 0, 0, 155, 855, 1, 0, 0, 0, 157, 857, 1, 0, 0, 0, 159, 859, 1, 0, 0, 0, 161, 861, 1, 0, 0, 0, 163, 866, 1, 0, 0, 0, 165, 887, 1, 0, 0, 0, 167, 889, 1, 0, 0, 0, 169, 894, 1, 0, 0, 0, 171, 915, 1, 0, 0, 0, 173, 917, 1, 0, 0, 0, 175, 925, 1, 0, 0, 0, 177, 927, 1, 0, 0, 0, 179, 931, 1, 0, 0, 0, 181, 935, 1, 0, 0, 0, 183, 939, 1, 0, 0, 0, 185, 944, 1, 0, 0, 0, 187, 949, 1, 0, 0, 0, 189, 953, 1, 0, 0, 0, 191, 957, 1, 0, 0, 0, 193, 961, 1, 0, 0, 0, 195, 966, 1, 0, 0, 0, 197, 970, 1, 0, 0, 0, 199, 974, 1, 0, 0, 0, 201, 978, 1, 0, 0, 0, 203, 982, 1, 0, 0, 0, 205, 986, 1, 0, 0, 0, 207, 998, 1, 0, 0, 0, 209, 1001, 1, 0, 0, 0, 211, 1005, 1, 0, 0, 0, 213, 1009, 1, 0, 0, 0, 215, 1013, 1, 0, 0, 0, 217, 1017, 1, 0, 0, 0, 219, 1021, 1, 0, 0, 0, 221, 1025, 1, 0, 0, 0, 223, 1030, 1, 0, 0, 0, 225, 1034, 1, 0, 0, 0, 227, 1038, 1, 0, 0, 0, 229, 1043, 1, 0, 0, 0, 231, 1052, 1, 0, 0, 0, 233, 1073, 1, 0, 0, 0, 235, 1077, 1, 0, 0, 0, 237, 1081, 1, 0, 0, 0, 239, 1085, 1, 0, 0, 0, 241, 1089, 1, 0, 0, 0, 243, 1093, 1, 0, 0, 0, 245, 1098, 1, 0, 0, 0, 247, 1102, 1, 0, 0, 0, 249, 1106, 1, 0, 0, 0, 251, 1110, 1, 0, 0, 0, 253, 1115, 1, 0, 0, 0, 255, 1120, 1, 0, 0, 0, 257, 1123, 1, 0, 0, 0, 259, 1127, 1, 0, 0, 0, 261, 1131, 1, 0, 0, 0, 263, 1135, 1, 0, 0, 0, 265, 1139, 1, 0, 0, 0, 267, 1144, 1, 0, 0, 0, 269, 1149, 1, 0, 0, 0, 271, 1154, 1, 0, 0, 0, 273, 1161, 1, 0, 0, 0, 275, 1170, 1, 0, 0, 0, 277, 1177, 1, 0, 0, 0, 279, 1181, 1, 0, 0, 0, 281, 1185, 1, 0, 0, 0, 283, 1189, 1, 0, 0, 0, 285, 1193, 1, 0, 0, 0, 287, 1199, 1, 0, 0, 0, 289, 1203, 1, 0, 0, 0, 291, 1207, 1, 0, 0, 0, 293, 1211, 1, 0, 0, 0, 295, 1215, 1, 0, 0, 0, 297, 1219, 1, 0, 0, 0, 299, 1223, 1, 0, 0, 0, 301, 1228, 1, 0, 0, 0, 303, 1233, 1, 0, 0, 0, 305, 1237, 1, 0, 0, 0, 307, 1241, 1, 0, 0, 0, 309, 1245, 1, 0, 0, 0, 311, 1250, 1, 0, 0, 0, 313, 1254, 1, 0, 0, 0, 315, 1259, 1, 0, 0, 0, 317, 1264, 1, 0, 0, 0, 319, 1268, 1, 0, 0, 0, 321, 1272, 1, 0, 0, 0, 323, 1276, 1, 0, 0, 0, 325, 1280, 1, 0, 0, 0, 327, 1284, 1, 0, 0, 0, 329, 1289, 1, 0, 0, 0, 331, 1294, 1, 0, 0, 0, 333, 1298, 1, 0, 0, 0, 335, 1302, 1, 0, 0, 0, 337, 1306, 1, 0, 0, 0, 339, 1311, 1, 0, 0, 0, 341, 1320, 1, 0, 0, 0, 343, 1324, 1, 0, 0, 0, 345, 1328, 1, 0, 0, 0, 347, 1332, 1, 0, 0, 0, 349, 1336, 1, 0, 0, 0, 351, 1341, 1, 0, 0, 0, 353, 1345, 1, 0, 0, 0, 355, 1349, 1, 0, 0, 0, 357, 1353, 1, 0, 0, 0, 359, 1358, 1, 0, 0, 0, 361, 1362, 1, 0, 0, 0, 363, 1366, 1, 0, 0, 0, 365, 1370, 1, 0, 0, 0, 367, 1374, 1, 0, 0, 0, 369, 1378, 1, 0, 0, 0, 371, 1384, 1, 0, 0, 0, 373, 1388, 1, 0, 0, 0, 375, 1392, 1, 0, 0, 0, 377, 1396, 1, 0, 0, 0, 379, 1400, 1, 0, 0, 0, 381, 1404, 1, 0, 0, 0, 383, 1408, 1, 0, 0, 0, 385, 1413, 1, 0, 0, 0, 387, 1419, 1, 0, 0, 0, 389, 1425, 1, 0, 0, 0, 391, 1429, 1, 0, 0, 0, 393, 1433, 1, 0, 0, 0, 395, 1437, 1, 0, 0, 0, 397, 1443, 1, 0, 0, 0, 399, 1449, 1, 0, 0, 0, 401, 1453, 1, 0, 0, 0, 403, 1457, 1, 0, 0, 0, 405, 1461, 1, 0, 0, 0, 407, 1467, 1, 0, 0, 0, 409, 1473, 1, 0, 0, 0, 411, 1479, 1, 0, 0, 0, 413, 414, 7, 0, 0, 0, 414, 415, 7, 1, 0, 0, 415, 416, 7, 2, 0, 0, 416, 417, 7, 2, 0, 0, 417, 418, 7, 3, 0, 0, 418, 419, 7, 4, 0, 0, 419, 420, 7, 5, 0, 0, 420, 421, 1, 0, 0, 0, 421, 422, 6, 0, 0, 0, 422, 16, 1, 0, 0, 0, 423, 424, 7, 0, 0, 0, 424, 425, 7, 6, 0, 0, 425, 426, 7, 7, 0, 0, 426, 427, 7, 8, 0, 0, 427, 428, 1, 0, 0, 0, 428, 429, 6, 1, 1, 0, 429, 18, 1, 0, 0, 0, 430, 431, 7, 3, 0, 0, 431, 432, 7, 9, 0, 0, 432, 433, 7, 6, 0, 0, 433, 434, 7, 1, 0, 0, 434, 435, 7, 4, 0, 0, 435, 436, 7, 10, 0, 0, 436, 437, 1, 0, 0, 0, 437, 438, 6, 2, 2, 0, 438, 20, 1, 0, 0, 0, 439, 440, 7, 3, 0, 0, 440, 441, 7, 11, 0, 0, 441, 442, 7, 12, 0, 0, 442, 443, 7, 13, 0, 0, 443, 444, 1, 0, 0, 0, 444, 445, 6, 3, 0, 0, 445, 22, 1, 0, 0, 0, 446, 447, 7, 3, 0, 0, 447, 448, 7, 14, 0, 0, 448, 449, 7, 8, 0, 0, 449, 450, 7, 13, 0, 0, 450, 451, 7, 12, 0, 0, 451, 452, 7, 1, 0, 0, 452, 453, 7, 9, 0, 0, 453, 454, 1, 0, 0, 0, 454, 455, 6, 4, 3, 0, 455, 24, 1, 0, 0, 0, 456, 457, 7, 15, 0, 0, 457, 458, 7, 6, 0, 0, 458, 459, 7, 7, 0, 0, 459, 460, 7, 16, 0, 0, 460, 461, 1, 0, 0, 0, 461, 462, 6, 5, 4, 0, 462, 26, 1, 0, 0, 0, 463, 464, 7, 17, 0, 0, 464, 465, 7, 6, 0, 0, 465, 466, 7, 7, 0, 0, 466, 467, 7, 18, 0, 0, 467, 468, 1, 0, 0, 0, 468, 469, 6, 6, 0, 0, 469, 28, 1, 0, 0, 0, 470, 471, 7, 18, 0, 0, 471, 472, 7, 3, 0, 0, 472, 473, 7, 3, 0, 0, 473, 474, 7, 8, 0, 0, 474, 475, 1, 0, 0, 0, 475, 476, 6, 7, 1, 0, 476, 30, 1, 0, 0, 0, 477, 478, 7, 13, 0, 0, 478, 479, 7, 1, 0, 0, 479, 480, 7, 16, 0, 0, 480, 481, 7, 1, 0, 0, 481, 482, 7, 5, 0, 0, 482, 483, 1, 0, 0, 0, 483, 484, 6, 8, 0, 0, 484, 32, 1, 0, 0, 0, 485, 486, 7, 16, 0, 0, 486, 487, 7, 11, 0, 0, 487, 488, 5, 95, 0, 0, 488, 489, 7, 3, 0, 0, 489, 490, 7, 14, 0, 0, 490, 491, 7, 8, 0, 0, 491, 492, 7, 12, 0, 0, 492, 493, 7, 9, 0, 0, 493, 494, 7, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 496, 6, 9, 5, 0, 496, 34, 1, 0, 0, 0, 497, 498, 7, 6, 0, 0, 498, 499, 7, 3, 0, 0, 499, 500, 7, 9, 0, 0, 500, 501, 7, 12, 0, 0, 501, 502, 7, 16, 0, 0, 502, 503, 7, 3, 0, 0, 503, 504, 1, 0, 0, 0, 504, 505, 6, 10, 6, 0, 505, 36, 1, 0, 0, 0, 506, 507, 7, 6, 0, 0, 507, 508, 7, 7, 0, 0, 508, 509, 7, 19, 0, 0, 509, 510, 1, 0, 0, 0, 510, 511, 6, 11, 0, 0, 511, 38, 1, 0, 0, 0, 512, 513, 7, 2, 0, 0, 513, 514, 7, 10, 0, 0, 514, 515, 7, 7, 0, 0, 515, 516, 7, 19, 0, 0, 516, 517, 1, 0, 0, 0, 517, 518, 6, 12, 7, 0, 518, 40, 1, 0, 0, 0, 519, 520, 7, 2, 0, 0, 520, 521, 7, 7, 0, 0, 521, 522, 7, 6, 0, 0, 522, 523, 7, 5, 0, 0, 523, 524, 1, 0, 0, 0, 524, 525, 6, 13, 0, 0, 525, 42, 1, 0, 0, 0, 526, 527, 7, 2, 0, 0, 527, 528, 7, 5, 0, 0, 528, 529, 7, 12, 0, 0, 529, 530, 7, 5, 0, 0, 530, 531, 7, 2, 0, 0, 531, 532, 1, 0, 0, 0, 532, 533, 6, 14, 0, 0, 533, 44, 1, 0, 0, 0, 534, 535, 7, 19, 0, 0, 535, 536, 7, 10, 0, 0, 536, 537, 7, 3, 0, 0, 537, 538, 7, 6, 0, 0, 538, 539, 7, 3, 0, 0, 539, 540, 1, 0, 0, 0, 540, 541, 6, 15, 0, 0, 541, 46, 1, 0, 0, 0, 542, 543, 4, 16, 0, 0, 543, 544, 7, 1, 0, 0, 544, 545, 7, 9, 0, 0, 545, 546, 7, 13, 0, 0, 546, 547, 7, 1, 0, 0, 547, 548, 7, 9, 0, 0, 548, 549, 7, 3, 0, 0, 549, 550, 7, 2, 0, 0, 550, 551, 7, 5, 0, 0, 551, 552, 7, 12, 0, 0, 552, 553, 7, 5, 0, 0, 553, 554, 7, 2, 0, 0, 554, 555, 1, 0, 0, 0, 555, 556, 6, 16, 0, 0, 556, 48, 1, 0, 0, 0, 557, 558, 4, 17, 1, 0, 558, 559, 7, 13, 0, 0, 559, 560, 7, 7, 0, 0, 560, 561, 7, 7, 0, 0, 561, 562, 7, 18, 0, 0, 562, 563, 7, 20, 0, 0, 563, 564, 7, 8, 0, 0, 564, 565, 1, 0, 0, 0, 565, 566, 6, 17, 8, 0, 566, 50, 1, 0, 0, 0, 567, 568, 4, 18, 2, 0, 568, 569, 7, 16, 0, 0, 569, 570, 7, 3, 0, 0, 570, 571, 7, 5, 0, 0, 571, 572, 7, 6, 0, 0, 572, 573, 7, 1, 0, 0, 573, 574, 7, 4, 0, 0, 574, 575, 7, 2, 0, 0, 575, 576, 1, 0, 0, 0, 576, 577, 6, 18, 9, 0, 577, 52, 1, 0, 0, 0, 578, 580, 8, 21, 0, 0, 579, 578, 1, 0, 0, 0, 580, 581, 1, 0, 0, 0, 581, 579, 1, 0, 0, 0, 581, 582, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 584, 6, 19, 0, 0, 584, 54, 1, 0, 0, 0, 585, 586, 5, 47, 0, 0, 586, 587, 5, 47, 0, 0, 587, 591, 1, 0, 0, 0, 588, 590, 8, 22, 0, 0, 589, 588, 1, 0, 0, 0, 590, 593, 1, 0, 0, 0, 591, 589, 1, 0, 0, 0, 591, 592, 1, 0, 0, 0, 592, 595, 1, 0, 0, 0, 593, 591, 1, 0, 0, 0, 594, 596, 5, 13, 0, 0, 595, 594, 1, 0, 0, 0, 595, 596, 1, 0, 0, 0, 596, 598, 1, 0, 0, 0, 597, 599, 5, 10, 0, 0, 598, 597, 1, 0, 0, 0, 598, 599, 1, 0, 0, 0, 599, 600, 1, 0, 0, 0, 600, 601, 6, 20, 10, 0, 601, 56, 1, 0, 0, 0, 602, 603, 5, 47, 0, 0, 603, 604, 5, 42, 0, 0, 604, 609, 1, 0, 0, 0, 605, 608, 3, 57, 21, 0, 606, 608, 9, 0, 0, 0, 607, 605, 1, 0, 0, 0, 607, 606, 1, 0, 0, 0, 608, 611, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 609, 607, 1, 0, 0, 0, 610, 612, 1, 0, 0, 0, 611, 609, 1, 0, 0, 0, 612, 613, 5, 42, 0, 0, 613, 614, 5, 47, 0, 0, 614, 615, 1, 0, 0, 0, 615, 616, 6, 21, 10, 0, 616, 58, 1, 0, 0, 0, 617, 619, 7, 23, 0, 0, 618, 617, 1, 0, 0, 0, 619, 620, 1, 0, 0, 0, 620, 618, 1, 0, 0, 0, 620, 621, 1, 0, 0, 0, 621, 622, 1, 0, 0, 0, 622, 623, 6, 22, 10, 0, 623, 60, 1, 0, 0, 0, 624, 625, 5, 58, 0, 0, 625, 62, 1, 0, 0, 0, 626, 627, 5, 124, 0, 0, 627, 628, 1, 0, 0, 0, 628, 629, 6, 24, 11, 0, 629, 64, 1, 0, 0, 0, 630, 631, 7, 24, 0, 0, 631, 66, 1, 0, 0, 0, 632, 633, 7, 25, 0, 0, 633, 68, 1, 0, 0, 0, 634, 635, 5, 92, 0, 0, 635, 636, 7, 26, 0, 0, 636, 70, 1, 0, 0, 0, 637, 638, 8, 27, 0, 0, 638, 72, 1, 0, 0, 0, 639, 641, 7, 3, 0, 0, 640, 642, 7, 28, 0, 0, 641, 640, 1, 0, 0, 0, 641, 642, 1, 0, 0, 0, 642, 644, 1, 0, 0, 0, 643, 645, 3, 65, 25, 0, 644, 643, 1, 0, 0, 0, 645, 646, 1, 0, 0, 0, 646, 644, 1, 0, 0, 0, 646, 647, 1, 0, 0, 0, 647, 74, 1, 0, 0, 0, 648, 649, 5, 64, 0, 0, 649, 76, 1, 0, 0, 0, 650, 651, 5, 96, 0, 0, 651, 78, 1, 0, 0, 0, 652, 656, 8, 29, 0, 0, 653, 654, 5, 96, 0, 0, 654, 656, 5, 96, 0, 0, 655, 652, 1, 0, 0, 0, 655, 653, 1, 0, 0, 0, 656, 80, 1, 0, 0, 0, 657, 658, 5, 95, 0, 0, 658, 82, 1, 0, 0, 0, 659, 663, 3, 67, 26, 0, 660, 663, 3, 65, 25, 0, 661, 663, 3, 81, 33, 0, 662, 659, 1, 0, 0, 0, 662, 660, 1, 0, 0, 0, 662, 661, 1, 0, 0, 0, 663, 84, 1, 0, 0, 0, 664, 669, 5, 34, 0, 0, 665, 668, 3, 69, 27, 0, 666, 668, 3, 71, 28, 0, 667, 665, 1, 0, 0, 0, 667, 666, 1, 0, 0, 0, 668, 671, 1, 0, 0, 0, 669, 667, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 672, 1, 0, 0, 0, 671, 669, 1, 0, 0, 0, 672, 694, 5, 34, 0, 0, 673, 674, 5, 34, 0, 0, 674, 675, 5, 34, 0, 0, 675, 676, 5, 34, 0, 0, 676, 680, 1, 0, 0, 0, 677, 679, 8, 22, 0, 0, 678, 677, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 681, 683, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 5, 34, 0, 0, 684, 685, 5, 34, 0, 0, 685, 686, 5, 34, 0, 0, 686, 688, 1, 0, 0, 0, 687, 689, 5, 34, 0, 0, 688, 687, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 691, 1, 0, 0, 0, 690, 692, 5, 34, 0, 0, 691, 690, 1, 0, 0, 0, 691, 692, 1, 0, 0, 0, 692, 694, 1, 0, 0, 0, 693, 664, 1, 0, 0, 0, 693, 673, 1, 0, 0, 0, 694, 86, 1, 0, 0, 0, 695, 697, 3, 65, 25, 0, 696, 695, 1, 0, 0, 0, 697, 698, 1, 0, 0, 0, 698, 696, 1, 0, 0, 0, 698, 699, 1, 0, 0, 0, 699, 88, 1, 0, 0, 0, 700, 702, 3, 65, 25, 0, 701, 700, 1, 0, 0, 0, 702, 703, 1, 0, 0, 0, 703, 701, 1, 0, 0, 0, 703, 704, 1, 0, 0, 0, 704, 705, 1, 0, 0, 0, 705, 709, 3, 105, 45, 0, 706, 708, 3, 65, 25, 0, 707, 706, 1, 0, 0, 0, 708, 711, 1, 0, 0, 0, 709, 707, 1, 0, 0, 0, 709, 710, 1, 0, 0, 0, 710, 743, 1, 0, 0, 0, 711, 709, 1, 0, 0, 0, 712, 714, 3, 105, 45, 0, 713, 715, 3, 65, 25, 0, 714, 713, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, 714, 1, 0, 0, 0, 716, 717, 1, 0, 0, 0, 717, 743, 1, 0, 0, 0, 718, 720, 3, 65, 25, 0, 719, 718, 1, 0, 0, 0, 720, 721, 1, 0, 0, 0, 721, 719, 1, 0, 0, 0, 721, 722, 1, 0, 0, 0, 722, 730, 1, 0, 0, 0, 723, 727, 3, 105, 45, 0, 724, 726, 3, 65, 25, 0, 725, 724, 1, 0, 0, 0, 726, 729, 1, 0, 0, 0, 727, 725, 1, 0, 0, 0, 727, 728, 1, 0, 0, 0, 728, 731, 1, 0, 0, 0, 729, 727, 1, 0, 0, 0, 730, 723, 1, 0, 0, 0, 730, 731, 1, 0, 0, 0, 731, 732, 1, 0, 0, 0, 732, 733, 3, 73, 29, 0, 733, 743, 1, 0, 0, 0, 734, 736, 3, 105, 45, 0, 735, 737, 3, 65, 25, 0, 736, 735, 1, 0, 0, 0, 737, 738, 1, 0, 0, 0, 738, 736, 1, 0, 0, 0, 738, 739, 1, 0, 0, 0, 739, 740, 1, 0, 0, 0, 740, 741, 3, 73, 29, 0, 741, 743, 1, 0, 0, 0, 742, 701, 1, 0, 0, 0, 742, 712, 1, 0, 0, 0, 742, 719, 1, 0, 0, 0, 742, 734, 1, 0, 0, 0, 743, 90, 1, 0, 0, 0, 744, 745, 7, 30, 0, 0, 745, 746, 7, 31, 0, 0, 746, 92, 1, 0, 0, 0, 747, 748, 7, 12, 0, 0, 748, 749, 7, 9, 0, 0, 749, 750, 7, 0, 0, 0, 750, 94, 1, 0, 0, 0, 751, 752, 7, 12, 0, 0, 752, 753, 7, 2, 0, 0, 753, 754, 7, 4, 0, 0, 754, 96, 1, 0, 0, 0, 755, 756, 5, 61, 0, 0, 756, 98, 1, 0, 0, 0, 757, 758, 5, 58, 0, 0, 758, 759, 5, 58, 0, 0, 759, 100, 1, 0, 0, 0, 760, 761, 5, 44, 0, 0, 761, 102, 1, 0, 0, 0, 762, 763, 7, 0, 0, 0, 763, 764, 7, 3, 0, 0, 764, 765, 7, 2, 0, 0, 765, 766, 7, 4, 0, 0, 766, 104, 1, 0, 0, 0, 767, 768, 5, 46, 0, 0, 768, 106, 1, 0, 0, 0, 769, 770, 7, 15, 0, 0, 770, 771, 7, 12, 0, 0, 771, 772, 7, 13, 0, 0, 772, 773, 7, 2, 0, 0, 773, 774, 7, 3, 0, 0, 774, 108, 1, 0, 0, 0, 775, 776, 7, 15, 0, 0, 776, 777, 7, 1, 0, 0, 777, 778, 7, 6, 0, 0, 778, 779, 7, 2, 0, 0, 779, 780, 7, 5, 0, 0, 780, 110, 1, 0, 0, 0, 781, 782, 7, 1, 0, 0, 782, 783, 7, 9, 0, 0, 783, 112, 1, 0, 0, 0, 784, 785, 7, 1, 0, 0, 785, 786, 7, 2, 0, 0, 786, 114, 1, 0, 0, 0, 787, 788, 7, 13, 0, 0, 788, 789, 7, 12, 0, 0, 789, 790, 7, 2, 0, 0, 790, 791, 7, 5, 0, 0, 791, 116, 1, 0, 0, 0, 792, 793, 7, 13, 0, 0, 793, 794, 7, 1, 0, 0, 794, 795, 7, 18, 0, 0, 795, 796, 7, 3, 0, 0, 796, 118, 1, 0, 0, 0, 797, 798, 5, 40, 0, 0, 798, 120, 1, 0, 0, 0, 799, 800, 7, 9, 0, 0, 800, 801, 7, 7, 0, 0, 801, 802, 7, 5, 0, 0, 802, 122, 1, 0, 0, 0, 803, 804, 7, 9, 0, 0, 804, 805, 7, 20, 0, 0, 805, 806, 7, 13, 0, 0, 806, 807, 7, 13, 0, 0, 807, 124, 1, 0, 0, 0, 808, 809, 7, 9, 0, 0, 809, 810, 7, 20, 0, 0, 810, 811, 7, 13, 0, 0, 811, 812, 7, 13, 0, 0, 812, 813, 7, 2, 0, 0, 813, 126, 1, 0, 0, 0, 814, 815, 7, 7, 0, 0, 815, 816, 7, 6, 0, 0, 816, 128, 1, 0, 0, 0, 817, 818, 5, 63, 0, 0, 818, 130, 1, 0, 0, 0, 819, 820, 7, 6, 0, 0, 820, 821, 7, 13, 0, 0, 821, 822, 7, 1, 0, 0, 822, 823, 7, 18, 0, 0, 823, 824, 7, 3, 0, 0, 824, 132, 1, 0, 0, 0, 825, 826, 5, 41, 0, 0, 826, 134, 1, 0, 0, 0, 827, 828, 7, 5, 0, 0, 828, 829, 7, 6, 0, 0, 829, 830, 7, 20, 0, 0, 830, 831, 7, 3, 0, 0, 831, 136, 1, 0, 0, 0, 832, 833, 5, 61, 0, 0, 833, 834, 5, 61, 0, 0, 834, 138, 1, 0, 0, 0, 835, 836, 5, 61, 0, 0, 836, 837, 5, 126, 0, 0, 837, 140, 1, 0, 0, 0, 838, 839, 5, 33, 0, 0, 839, 840, 5, 61, 0, 0, 840, 142, 1, 0, 0, 0, 841, 842, 5, 60, 0, 0, 842, 144, 1, 0, 0, 0, 843, 844, 5, 60, 0, 0, 844, 845, 5, 61, 0, 0, 845, 146, 1, 0, 0, 0, 846, 847, 5, 62, 0, 0, 847, 148, 1, 0, 0, 0, 848, 849, 5, 62, 0, 0, 849, 850, 5, 61, 0, 0, 850, 150, 1, 0, 0, 0, 851, 852, 5, 43, 0, 0, 852, 152, 1, 0, 0, 0, 853, 854, 5, 45, 0, 0, 854, 154, 1, 0, 0, 0, 855, 856, 5, 42, 0, 0, 856, 156, 1, 0, 0, 0, 857, 858, 5, 47, 0, 0, 858, 158, 1, 0, 0, 0, 859, 860, 5, 37, 0, 0, 860, 160, 1, 0, 0, 0, 861, 862, 4, 73, 3, 0, 862, 863, 3, 61, 23, 0, 863, 864, 1, 0, 0, 0, 864, 865, 6, 73, 12, 0, 865, 162, 1, 0, 0, 0, 866, 867, 3, 45, 15, 0, 867, 868, 1, 0, 0, 0, 868, 869, 6, 74, 13, 0, 869, 164, 1, 0, 0, 0, 870, 873, 3, 129, 57, 0, 871, 874, 3, 67, 26, 0, 872, 874, 3, 81, 33, 0, 873, 871, 1, 0, 0, 0, 873, 872, 1, 0, 0, 0, 874, 878, 1, 0, 0, 0, 875, 877, 3, 83, 34, 0, 876, 875, 1, 0, 0, 0, 877, 880, 1, 0, 0, 0, 878, 876, 1, 0, 0, 0, 878, 879, 1, 0, 0, 0, 879, 888, 1, 0, 0, 0, 880, 878, 1, 0, 0, 0, 881, 883, 3, 129, 57, 0, 882, 884, 3, 65, 25, 0, 883, 882, 1, 0, 0, 0, 884, 885, 1, 0, 0, 0, 885, 883, 1, 0, 0, 0, 885, 886, 1, 0, 0, 0, 886, 888, 1, 0, 0, 0, 887, 870, 1, 0, 0, 0, 887, 881, 1, 0, 0, 0, 888, 166, 1, 0, 0, 0, 889, 890, 5, 91, 0, 0, 890, 891, 1, 0, 0, 0, 891, 892, 6, 76, 0, 0, 892, 893, 6, 76, 0, 0, 893, 168, 1, 0, 0, 0, 894, 895, 5, 93, 0, 0, 895, 896, 1, 0, 0, 0, 896, 897, 6, 77, 11, 0, 897, 898, 6, 77, 11, 0, 898, 170, 1, 0, 0, 0, 899, 903, 3, 67, 26, 0, 900, 902, 3, 83, 34, 0, 901, 900, 1, 0, 0, 0, 902, 905, 1, 0, 0, 0, 903, 901, 1, 0, 0, 0, 903, 904, 1, 0, 0, 0, 904, 916, 1, 0, 0, 0, 905, 903, 1, 0, 0, 0, 906, 909, 3, 81, 33, 0, 907, 909, 3, 75, 30, 0, 908, 906, 1, 0, 0, 0, 908, 907, 1, 0, 0, 0, 909, 911, 1, 0, 0, 0, 910, 912, 3, 83, 34, 0, 911, 910, 1, 0, 0, 0, 912, 913, 1, 0, 0, 0, 913, 911, 1, 0, 0, 0, 913, 914, 1, 0, 0, 0, 914, 916, 1, 0, 0, 0, 915, 899, 1, 0, 0, 0, 915, 908, 1, 0, 0, 0, 916, 172, 1, 0, 0, 0, 917, 919, 3, 77, 31, 0, 918, 920, 3, 79, 32, 0, 919, 918, 1, 0, 0, 0, 920, 921, 1, 0, 0, 0, 921, 919, 1, 0, 0, 0, 921, 922, 1, 0, 0, 0, 922, 923, 1, 0, 0, 0, 923, 924, 3, 77, 31, 0, 924, 174, 1, 0, 0, 0, 925, 926, 3, 173, 79, 0, 926, 176, 1, 0, 0, 0, 927, 928, 3, 55, 20, 0, 928, 929, 1, 0, 0, 0, 929, 930, 6, 81, 10, 0, 930, 178, 1, 0, 0, 0, 931, 932, 3, 57, 21, 0, 932, 933, 1, 0, 0, 0, 933, 934, 6, 82, 10, 0, 934, 180, 1, 0, 0, 0, 935, 936, 3, 59, 22, 0, 936, 937, 1, 0, 0, 0, 937, 938, 6, 83, 10, 0, 938, 182, 1, 0, 0, 0, 939, 940, 3, 167, 76, 0, 940, 941, 1, 0, 0, 0, 941, 942, 6, 84, 14, 0, 942, 943, 6, 84, 15, 0, 943, 184, 1, 0, 0, 0, 944, 945, 3, 63, 24, 0, 945, 946, 1, 0, 0, 0, 946, 947, 6, 85, 16, 0, 947, 948, 6, 85, 11, 0, 948, 186, 1, 0, 0, 0, 949, 950, 3, 59, 22, 0, 950, 951, 1, 0, 0, 0, 951, 952, 6, 86, 10, 0, 952, 188, 1, 0, 0, 0, 953, 954, 3, 55, 20, 0, 954, 955, 1, 0, 0, 0, 955, 956, 6, 87, 10, 0, 956, 190, 1, 0, 0, 0, 957, 958, 3, 57, 21, 0, 958, 959, 1, 0, 0, 0, 959, 960, 6, 88, 10, 0, 960, 192, 1, 0, 0, 0, 961, 962, 3, 63, 24, 0, 962, 963, 1, 0, 0, 0, 963, 964, 6, 89, 16, 0, 964, 965, 6, 89, 11, 0, 965, 194, 1, 0, 0, 0, 966, 967, 3, 167, 76, 0, 967, 968, 1, 0, 0, 0, 968, 969, 6, 90, 14, 0, 969, 196, 1, 0, 0, 0, 970, 971, 3, 169, 77, 0, 971, 972, 1, 0, 0, 0, 972, 973, 6, 91, 17, 0, 973, 198, 1, 0, 0, 0, 974, 975, 3, 61, 23, 0, 975, 976, 1, 0, 0, 0, 976, 977, 6, 92, 12, 0, 977, 200, 1, 0, 0, 0, 978, 979, 3, 101, 43, 0, 979, 980, 1, 0, 0, 0, 980, 981, 6, 93, 18, 0, 981, 202, 1, 0, 0, 0, 982, 983, 3, 97, 41, 0, 983, 984, 1, 0, 0, 0, 984, 985, 6, 94, 19, 0, 985, 204, 1, 0, 0, 0, 986, 987, 7, 16, 0, 0, 987, 988, 7, 3, 0, 0, 988, 989, 7, 5, 0, 0, 989, 990, 7, 12, 0, 0, 990, 991, 7, 0, 0, 0, 991, 992, 7, 12, 0, 0, 992, 993, 7, 5, 0, 0, 993, 994, 7, 12, 0, 0, 994, 206, 1, 0, 0, 0, 995, 999, 8, 32, 0, 0, 996, 997, 5, 47, 0, 0, 997, 999, 8, 33, 0, 0, 998, 995, 1, 0, 0, 0, 998, 996, 1, 0, 0, 0, 999, 208, 1, 0, 0, 0, 1000, 1002, 3, 207, 96, 0, 1001, 1000, 1, 0, 0, 0, 1002, 1003, 1, 0, 0, 0, 1003, 1001, 1, 0, 0, 0, 1003, 1004, 1, 0, 0, 0, 1004, 210, 1, 0, 0, 0, 1005, 1006, 3, 209, 97, 0, 1006, 1007, 1, 0, 0, 0, 1007, 1008, 6, 98, 20, 0, 1008, 212, 1, 0, 0, 0, 1009, 1010, 3, 85, 35, 0, 1010, 1011, 1, 0, 0, 0, 1011, 1012, 6, 99, 21, 0, 1012, 214, 1, 0, 0, 0, 1013, 1014, 3, 55, 20, 0, 1014, 1015, 1, 0, 0, 0, 1015, 1016, 6, 100, 10, 0, 1016, 216, 1, 0, 0, 0, 1017, 1018, 3, 57, 21, 0, 1018, 1019, 1, 0, 0, 0, 1019, 1020, 6, 101, 10, 0, 1020, 218, 1, 0, 0, 0, 1021, 1022, 3, 59, 22, 0, 1022, 1023, 1, 0, 0, 0, 1023, 1024, 6, 102, 10, 0, 1024, 220, 1, 0, 0, 0, 1025, 1026, 3, 63, 24, 0, 1026, 1027, 1, 0, 0, 0, 1027, 1028, 6, 103, 16, 0, 1028, 1029, 6, 103, 11, 0, 1029, 222, 1, 0, 0, 0, 1030, 1031, 3, 105, 45, 0, 1031, 1032, 1, 0, 0, 0, 1032, 1033, 6, 104, 22, 0, 1033, 224, 1, 0, 0, 0, 1034, 1035, 3, 101, 43, 0, 1035, 1036, 1, 0, 0, 0, 1036, 1037, 6, 105, 18, 0, 1037, 226, 1, 0, 0, 0, 1038, 1039, 4, 106, 4, 0, 1039, 1040, 3, 129, 57, 0, 1040, 1041, 1, 0, 0, 0, 1041, 1042, 6, 106, 23, 0, 1042, 228, 1, 0, 0, 0, 1043, 1044, 4, 107, 5, 0, 1044, 1045, 3, 165, 75, 0, 1045, 1046, 1, 0, 0, 0, 1046, 1047, 6, 107, 24, 0, 1047, 230, 1, 0, 0, 0, 1048, 1053, 3, 67, 26, 0, 1049, 1053, 3, 65, 25, 0, 1050, 1053, 3, 81, 33, 0, 1051, 1053, 3, 155, 70, 0, 1052, 1048, 1, 0, 0, 0, 1052, 1049, 1, 0, 0, 0, 1052, 1050, 1, 0, 0, 0, 1052, 1051, 1, 0, 0, 0, 1053, 232, 1, 0, 0, 0, 1054, 1057, 3, 67, 26, 0, 1055, 1057, 3, 155, 70, 0, 1056, 1054, 1, 0, 0, 0, 1056, 1055, 1, 0, 0, 0, 1057, 1061, 1, 0, 0, 0, 1058, 1060, 3, 231, 108, 0, 1059, 1058, 1, 0, 0, 0, 1060, 1063, 1, 0, 0, 0, 1061, 1059, 1, 0, 0, 0, 1061, 1062, 1, 0, 0, 0, 1062, 1074, 1, 0, 0, 0, 1063, 1061, 1, 0, 0, 0, 1064, 1067, 3, 81, 33, 0, 1065, 1067, 3, 75, 30, 0, 1066, 1064, 1, 0, 0, 0, 1066, 1065, 1, 0, 0, 0, 1067, 1069, 1, 0, 0, 0, 1068, 1070, 3, 231, 108, 0, 1069, 1068, 1, 0, 0, 0, 1070, 1071, 1, 0, 0, 0, 1071, 1069, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1074, 1, 0, 0, 0, 1073, 1056, 1, 0, 0, 0, 1073, 1066, 1, 0, 0, 0, 1074, 234, 1, 0, 0, 0, 1075, 1078, 3, 233, 109, 0, 1076, 1078, 3, 173, 79, 0, 1077, 1075, 1, 0, 0, 0, 1077, 1076, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1077, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 236, 1, 0, 0, 0, 1081, 1082, 3, 55, 20, 0, 1082, 1083, 1, 0, 0, 0, 1083, 1084, 6, 111, 10, 0, 1084, 238, 1, 0, 0, 0, 1085, 1086, 3, 57, 21, 0, 1086, 1087, 1, 0, 0, 0, 1087, 1088, 6, 112, 10, 0, 1088, 240, 1, 0, 0, 0, 1089, 1090, 3, 59, 22, 0, 1090, 1091, 1, 0, 0, 0, 1091, 1092, 6, 113, 10, 0, 1092, 242, 1, 0, 0, 0, 1093, 1094, 3, 63, 24, 0, 1094, 1095, 1, 0, 0, 0, 1095, 1096, 6, 114, 16, 0, 1096, 1097, 6, 114, 11, 0, 1097, 244, 1, 0, 0, 0, 1098, 1099, 3, 97, 41, 0, 1099, 1100, 1, 0, 0, 0, 1100, 1101, 6, 115, 19, 0, 1101, 246, 1, 0, 0, 0, 1102, 1103, 3, 101, 43, 0, 1103, 1104, 1, 0, 0, 0, 1104, 1105, 6, 116, 18, 0, 1105, 248, 1, 0, 0, 0, 1106, 1107, 3, 105, 45, 0, 1107, 1108, 1, 0, 0, 0, 1108, 1109, 6, 117, 22, 0, 1109, 250, 1, 0, 0, 0, 1110, 1111, 4, 118, 6, 0, 1111, 1112, 3, 129, 57, 0, 1112, 1113, 1, 0, 0, 0, 1113, 1114, 6, 118, 23, 0, 1114, 252, 1, 0, 0, 0, 1115, 1116, 4, 119, 7, 0, 1116, 1117, 3, 165, 75, 0, 1117, 1118, 1, 0, 0, 0, 1118, 1119, 6, 119, 24, 0, 1119, 254, 1, 0, 0, 0, 1120, 1121, 7, 12, 0, 0, 1121, 1122, 7, 2, 0, 0, 1122, 256, 1, 0, 0, 0, 1123, 1124, 3, 235, 110, 0, 1124, 1125, 1, 0, 0, 0, 1125, 1126, 6, 121, 25, 0, 1126, 258, 1, 0, 0, 0, 1127, 1128, 3, 55, 20, 0, 1128, 1129, 1, 0, 0, 0, 1129, 1130, 6, 122, 10, 0, 1130, 260, 1, 0, 0, 0, 1131, 1132, 3, 57, 21, 0, 1132, 1133, 1, 0, 0, 0, 1133, 1134, 6, 123, 10, 0, 1134, 262, 1, 0, 0, 0, 1135, 1136, 3, 59, 22, 0, 1136, 1137, 1, 0, 0, 0, 1137, 1138, 6, 124, 10, 0, 1138, 264, 1, 0, 0, 0, 1139, 1140, 3, 63, 24, 0, 1140, 1141, 1, 0, 0, 0, 1141, 1142, 6, 125, 16, 0, 1142, 1143, 6, 125, 11, 0, 1143, 266, 1, 0, 0, 0, 1144, 1145, 3, 167, 76, 0, 1145, 1146, 1, 0, 0, 0, 1146, 1147, 6, 126, 14, 0, 1147, 1148, 6, 126, 26, 0, 1148, 268, 1, 0, 0, 0, 1149, 1150, 7, 7, 0, 0, 1150, 1151, 7, 9, 0, 0, 1151, 1152, 1, 0, 0, 0, 1152, 1153, 6, 127, 27, 0, 1153, 270, 1, 0, 0, 0, 1154, 1155, 7, 19, 0, 0, 1155, 1156, 7, 1, 0, 0, 1156, 1157, 7, 5, 0, 0, 1157, 1158, 7, 10, 0, 0, 1158, 1159, 1, 0, 0, 0, 1159, 1160, 6, 128, 27, 0, 1160, 272, 1, 0, 0, 0, 1161, 1162, 8, 34, 0, 0, 1162, 274, 1, 0, 0, 0, 1163, 1165, 3, 273, 129, 0, 1164, 1163, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1164, 1, 0, 0, 0, 1166, 1167, 1, 0, 0, 0, 1167, 1168, 1, 0, 0, 0, 1168, 1169, 3, 61, 23, 0, 1169, 1171, 1, 0, 0, 0, 1170, 1164, 1, 0, 0, 0, 1170, 1171, 1, 0, 0, 0, 1171, 1173, 1, 0, 0, 0, 1172, 1174, 3, 273, 129, 0, 1173, 1172, 1, 0, 0, 0, 1174, 1175, 1, 0, 0, 0, 1175, 1173, 1, 0, 0, 0, 1175, 1176, 1, 0, 0, 0, 1176, 276, 1, 0, 0, 0, 1177, 1178, 3, 275, 130, 0, 1178, 1179, 1, 0, 0, 0, 1179, 1180, 6, 131, 28, 0, 1180, 278, 1, 0, 0, 0, 1181, 1182, 3, 55, 20, 0, 1182, 1183, 1, 0, 0, 0, 1183, 1184, 6, 132, 10, 0, 1184, 280, 1, 0, 0, 0, 1185, 1186, 3, 57, 21, 0, 1186, 1187, 1, 0, 0, 0, 1187, 1188, 6, 133, 10, 0, 1188, 282, 1, 0, 0, 0, 1189, 1190, 3, 59, 22, 0, 1190, 1191, 1, 0, 0, 0, 1191, 1192, 6, 134, 10, 0, 1192, 284, 1, 0, 0, 0, 1193, 1194, 3, 63, 24, 0, 1194, 1195, 1, 0, 0, 0, 1195, 1196, 6, 135, 16, 0, 1196, 1197, 6, 135, 11, 0, 1197, 1198, 6, 135, 11, 0, 1198, 286, 1, 0, 0, 0, 1199, 1200, 3, 97, 41, 0, 1200, 1201, 1, 0, 0, 0, 1201, 1202, 6, 136, 19, 0, 1202, 288, 1, 0, 0, 0, 1203, 1204, 3, 101, 43, 0, 1204, 1205, 1, 0, 0, 0, 1205, 1206, 6, 137, 18, 0, 1206, 290, 1, 0, 0, 0, 1207, 1208, 3, 105, 45, 0, 1208, 1209, 1, 0, 0, 0, 1209, 1210, 6, 138, 22, 0, 1210, 292, 1, 0, 0, 0, 1211, 1212, 3, 271, 128, 0, 1212, 1213, 1, 0, 0, 0, 1213, 1214, 6, 139, 29, 0, 1214, 294, 1, 0, 0, 0, 1215, 1216, 3, 235, 110, 0, 1216, 1217, 1, 0, 0, 0, 1217, 1218, 6, 140, 25, 0, 1218, 296, 1, 0, 0, 0, 1219, 1220, 3, 175, 80, 0, 1220, 1221, 1, 0, 0, 0, 1221, 1222, 6, 141, 30, 0, 1222, 298, 1, 0, 0, 0, 1223, 1224, 4, 142, 8, 0, 1224, 1225, 3, 129, 57, 0, 1225, 1226, 1, 0, 0, 0, 1226, 1227, 6, 142, 23, 0, 1227, 300, 1, 0, 0, 0, 1228, 1229, 4, 143, 9, 0, 1229, 1230, 3, 165, 75, 0, 1230, 1231, 1, 0, 0, 0, 1231, 1232, 6, 143, 24, 0, 1232, 302, 1, 0, 0, 0, 1233, 1234, 3, 55, 20, 0, 1234, 1235, 1, 0, 0, 0, 1235, 1236, 6, 144, 10, 0, 1236, 304, 1, 0, 0, 0, 1237, 1238, 3, 57, 21, 0, 1238, 1239, 1, 0, 0, 0, 1239, 1240, 6, 145, 10, 0, 1240, 306, 1, 0, 0, 0, 1241, 1242, 3, 59, 22, 0, 1242, 1243, 1, 0, 0, 0, 1243, 1244, 6, 146, 10, 0, 1244, 308, 1, 0, 0, 0, 1245, 1246, 3, 63, 24, 0, 1246, 1247, 1, 0, 0, 0, 1247, 1248, 6, 147, 16, 0, 1248, 1249, 6, 147, 11, 0, 1249, 310, 1, 0, 0, 0, 1250, 1251, 3, 105, 45, 0, 1251, 1252, 1, 0, 0, 0, 1252, 1253, 6, 148, 22, 0, 1253, 312, 1, 0, 0, 0, 1254, 1255, 4, 149, 10, 0, 1255, 1256, 3, 129, 57, 0, 1256, 1257, 1, 0, 0, 0, 1257, 1258, 6, 149, 23, 0, 1258, 314, 1, 0, 0, 0, 1259, 1260, 4, 150, 11, 0, 1260, 1261, 3, 165, 75, 0, 1261, 1262, 1, 0, 0, 0, 1262, 1263, 6, 150, 24, 0, 1263, 316, 1, 0, 0, 0, 1264, 1265, 3, 175, 80, 0, 1265, 1266, 1, 0, 0, 0, 1266, 1267, 6, 151, 30, 0, 1267, 318, 1, 0, 0, 0, 1268, 1269, 3, 171, 78, 0, 1269, 1270, 1, 0, 0, 0, 1270, 1271, 6, 152, 31, 0, 1271, 320, 1, 0, 0, 0, 1272, 1273, 3, 55, 20, 0, 1273, 1274, 1, 0, 0, 0, 1274, 1275, 6, 153, 10, 0, 1275, 322, 1, 0, 0, 0, 1276, 1277, 3, 57, 21, 0, 1277, 1278, 1, 0, 0, 0, 1278, 1279, 6, 154, 10, 0, 1279, 324, 1, 0, 0, 0, 1280, 1281, 3, 59, 22, 0, 1281, 1282, 1, 0, 0, 0, 1282, 1283, 6, 155, 10, 0, 1283, 326, 1, 0, 0, 0, 1284, 1285, 3, 63, 24, 0, 1285, 1286, 1, 0, 0, 0, 1286, 1287, 6, 156, 16, 0, 1287, 1288, 6, 156, 11, 0, 1288, 328, 1, 0, 0, 0, 1289, 1290, 7, 1, 0, 0, 1290, 1291, 7, 9, 0, 0, 1291, 1292, 7, 15, 0, 0, 1292, 1293, 7, 7, 0, 0, 1293, 330, 1, 0, 0, 0, 1294, 1295, 3, 55, 20, 0, 1295, 1296, 1, 0, 0, 0, 1296, 1297, 6, 158, 10, 0, 1297, 332, 1, 0, 0, 0, 1298, 1299, 3, 57, 21, 0, 1299, 1300, 1, 0, 0, 0, 1300, 1301, 6, 159, 10, 0, 1301, 334, 1, 0, 0, 0, 1302, 1303, 3, 59, 22, 0, 1303, 1304, 1, 0, 0, 0, 1304, 1305, 6, 160, 10, 0, 1305, 336, 1, 0, 0, 0, 1306, 1307, 3, 169, 77, 0, 1307, 1308, 1, 0, 0, 0, 1308, 1309, 6, 161, 17, 0, 1309, 1310, 6, 161, 11, 0, 1310, 338, 1, 0, 0, 0, 1311, 1312, 3, 61, 23, 0, 1312, 1313, 1, 0, 0, 0, 1313, 1314, 6, 162, 12, 0, 1314, 340, 1, 0, 0, 0, 1315, 1321, 3, 75, 30, 0, 1316, 1321, 3, 65, 25, 0, 1317, 1321, 3, 105, 45, 0, 1318, 1321, 3, 67, 26, 0, 1319, 1321, 3, 81, 33, 0, 1320, 1315, 1, 0, 0, 0, 1320, 1316, 1, 0, 0, 0, 1320, 1317, 1, 0, 0, 0, 1320, 1318, 1, 0, 0, 0, 1320, 1319, 1, 0, 0, 0, 1321, 1322, 1, 0, 0, 0, 1322, 1320, 1, 0, 0, 0, 1322, 1323, 1, 0, 0, 0, 1323, 342, 1, 0, 0, 0, 1324, 1325, 3, 55, 20, 0, 1325, 1326, 1, 0, 0, 0, 1326, 1327, 6, 164, 10, 0, 1327, 344, 1, 0, 0, 0, 1328, 1329, 3, 57, 21, 0, 1329, 1330, 1, 0, 0, 0, 1330, 1331, 6, 165, 10, 0, 1331, 346, 1, 0, 0, 0, 1332, 1333, 3, 59, 22, 0, 1333, 1334, 1, 0, 0, 0, 1334, 1335, 6, 166, 10, 0, 1335, 348, 1, 0, 0, 0, 1336, 1337, 3, 63, 24, 0, 1337, 1338, 1, 0, 0, 0, 1338, 1339, 6, 167, 16, 0, 1339, 1340, 6, 167, 11, 0, 1340, 350, 1, 0, 0, 0, 1341, 1342, 3, 61, 23, 0, 1342, 1343, 1, 0, 0, 0, 1343, 1344, 6, 168, 12, 0, 1344, 352, 1, 0, 0, 0, 1345, 1346, 3, 101, 43, 0, 1346, 1347, 1, 0, 0, 0, 1347, 1348, 6, 169, 18, 0, 1348, 354, 1, 0, 0, 0, 1349, 1350, 3, 105, 45, 0, 1350, 1351, 1, 0, 0, 0, 1351, 1352, 6, 170, 22, 0, 1352, 356, 1, 0, 0, 0, 1353, 1354, 3, 269, 127, 0, 1354, 1355, 1, 0, 0, 0, 1355, 1356, 6, 171, 32, 0, 1356, 1357, 6, 171, 33, 0, 1357, 358, 1, 0, 0, 0, 1358, 1359, 3, 209, 97, 0, 1359, 1360, 1, 0, 0, 0, 1360, 1361, 6, 172, 20, 0, 1361, 360, 1, 0, 0, 0, 1362, 1363, 3, 85, 35, 0, 1363, 1364, 1, 0, 0, 0, 1364, 1365, 6, 173, 21, 0, 1365, 362, 1, 0, 0, 0, 1366, 1367, 3, 55, 20, 0, 1367, 1368, 1, 0, 0, 0, 1368, 1369, 6, 174, 10, 0, 1369, 364, 1, 0, 0, 0, 1370, 1371, 3, 57, 21, 0, 1371, 1372, 1, 0, 0, 0, 1372, 1373, 6, 175, 10, 0, 1373, 366, 1, 0, 0, 0, 1374, 1375, 3, 59, 22, 0, 1375, 1376, 1, 0, 0, 0, 1376, 1377, 6, 176, 10, 0, 1377, 368, 1, 0, 0, 0, 1378, 1379, 3, 63, 24, 0, 1379, 1380, 1, 0, 0, 0, 1380, 1381, 6, 177, 16, 0, 1381, 1382, 6, 177, 11, 0, 1382, 1383, 6, 177, 11, 0, 1383, 370, 1, 0, 0, 0, 1384, 1385, 3, 101, 43, 0, 1385, 1386, 1, 0, 0, 0, 1386, 1387, 6, 178, 18, 0, 1387, 372, 1, 0, 0, 0, 1388, 1389, 3, 105, 45, 0, 1389, 1390, 1, 0, 0, 0, 1390, 1391, 6, 179, 22, 0, 1391, 374, 1, 0, 0, 0, 1392, 1393, 3, 235, 110, 0, 1393, 1394, 1, 0, 0, 0, 1394, 1395, 6, 180, 25, 0, 1395, 376, 1, 0, 0, 0, 1396, 1397, 3, 55, 20, 0, 1397, 1398, 1, 0, 0, 0, 1398, 1399, 6, 181, 10, 0, 1399, 378, 1, 0, 0, 0, 1400, 1401, 3, 57, 21, 0, 1401, 1402, 1, 0, 0, 0, 1402, 1403, 6, 182, 10, 0, 1403, 380, 1, 0, 0, 0, 1404, 1405, 3, 59, 22, 0, 1405, 1406, 1, 0, 0, 0, 1406, 1407, 6, 183, 10, 0, 1407, 382, 1, 0, 0, 0, 1408, 1409, 3, 63, 24, 0, 1409, 1410, 1, 0, 0, 0, 1410, 1411, 6, 184, 16, 0, 1411, 1412, 6, 184, 11, 0, 1412, 384, 1, 0, 0, 0, 1413, 1414, 3, 209, 97, 0, 1414, 1415, 1, 0, 0, 0, 1415, 1416, 6, 185, 20, 0, 1416, 1417, 6, 185, 11, 0, 1417, 1418, 6, 185, 34, 0, 1418, 386, 1, 0, 0, 0, 1419, 1420, 3, 85, 35, 0, 1420, 1421, 1, 0, 0, 0, 1421, 1422, 6, 186, 21, 0, 1422, 1423, 6, 186, 11, 0, 1423, 1424, 6, 186, 34, 0, 1424, 388, 1, 0, 0, 0, 1425, 1426, 3, 55, 20, 0, 1426, 1427, 1, 0, 0, 0, 1427, 1428, 6, 187, 10, 0, 1428, 390, 1, 0, 0, 0, 1429, 1430, 3, 57, 21, 0, 1430, 1431, 1, 0, 0, 0, 1431, 1432, 6, 188, 10, 0, 1432, 392, 1, 0, 0, 0, 1433, 1434, 3, 59, 22, 0, 1434, 1435, 1, 0, 0, 0, 1435, 1436, 6, 189, 10, 0, 1436, 394, 1, 0, 0, 0, 1437, 1438, 3, 61, 23, 0, 1438, 1439, 1, 0, 0, 0, 1439, 1440, 6, 190, 12, 0, 1440, 1441, 6, 190, 11, 0, 1441, 1442, 6, 190, 9, 0, 1442, 396, 1, 0, 0, 0, 1443, 1444, 3, 101, 43, 0, 1444, 1445, 1, 0, 0, 0, 1445, 1446, 6, 191, 18, 0, 1446, 1447, 6, 191, 11, 0, 1447, 1448, 6, 191, 9, 0, 1448, 398, 1, 0, 0, 0, 1449, 1450, 3, 55, 20, 0, 1450, 1451, 1, 0, 0, 0, 1451, 1452, 6, 192, 10, 0, 1452, 400, 1, 0, 0, 0, 1453, 1454, 3, 57, 21, 0, 1454, 1455, 1, 0, 0, 0, 1455, 1456, 6, 193, 10, 0, 1456, 402, 1, 0, 0, 0, 1457, 1458, 3, 59, 22, 0, 1458, 1459, 1, 0, 0, 0, 1459, 1460, 6, 194, 10, 0, 1460, 404, 1, 0, 0, 0, 1461, 1462, 3, 175, 80, 0, 1462, 1463, 1, 0, 0, 0, 1463, 1464, 6, 195, 11, 0, 1464, 1465, 6, 195, 0, 0, 1465, 1466, 6, 195, 30, 0, 1466, 406, 1, 0, 0, 0, 1467, 1468, 3, 171, 78, 0, 1468, 1469, 1, 0, 0, 0, 1469, 1470, 6, 196, 11, 0, 1470, 1471, 6, 196, 0, 0, 1471, 1472, 6, 196, 31, 0, 1472, 408, 1, 0, 0, 0, 1473, 1474, 3, 91, 38, 0, 1474, 1475, 1, 0, 0, 0, 1475, 1476, 6, 197, 11, 0, 1476, 1477, 6, 197, 0, 0, 1477, 1478, 6, 197, 35, 0, 1478, 410, 1, 0, 0, 0, 1479, 1480, 3, 63, 24, 0, 1480, 1481, 1, 0, 0, 0, 1481, 1482, 6, 198, 16, 0, 1482, 1483, 6, 198, 11, 0, 1483, 412, 1, 0, 0, 0, 65, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 581, 591, 595, 598, 607, 609, 620, 641, 646, 655, 662, 667, 669, 680, 688, 691, 693, 698, 703, 709, 716, 721, 727, 730, 738, 742, 873, 878, 885, 887, 903, 908, 913, 915, 921, 998, 1003, 1052, 1056, 1061, 1066, 1071, 1073, 1077, 1079, 1166, 1170, 1175, 1320, 1322, 36, 5, 1, 0, 5, 4, 0, 5, 6, 0, 5, 2, 0, 5, 3, 0, 5, 8, 0, 5, 5, 0, 5, 9, 0, 5, 11, 0, 5, 13, 0, 0, 1, 0, 4, 0, 0, 7, 24, 0, 7, 16, 0, 7, 65, 0, 5, 0, 0, 7, 25, 0, 7, 66, 0, 7, 34, 0, 7, 32, 0, 7, 76, 0, 7, 26, 0, 7, 36, 0, 7, 48, 0, 7, 64, 0, 7, 80, 0, 5, 10, 0, 5, 7, 0, 7, 90, 0, 7, 89, 0, 7, 68, 0, 7, 67, 0, 7, 88, 0, 5, 12, 0, 5, 14, 0, 7, 29, 0] \ No newline at end of file +[4, 0, 128, 1601, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 2, 175, 7, 175, 2, 176, 7, 176, 2, 177, 7, 177, 2, 178, 7, 178, 2, 179, 7, 179, 2, 180, 7, 180, 2, 181, 7, 181, 2, 182, 7, 182, 2, 183, 7, 183, 2, 184, 7, 184, 2, 185, 7, 185, 2, 186, 7, 186, 2, 187, 7, 187, 2, 188, 7, 188, 2, 189, 7, 189, 2, 190, 7, 190, 2, 191, 7, 191, 2, 192, 7, 192, 2, 193, 7, 193, 2, 194, 7, 194, 2, 195, 7, 195, 2, 196, 7, 196, 2, 197, 7, 197, 2, 198, 7, 198, 2, 199, 7, 199, 2, 200, 7, 200, 2, 201, 7, 201, 2, 202, 7, 202, 2, 203, 7, 203, 2, 204, 7, 204, 2, 205, 7, 205, 2, 206, 7, 206, 2, 207, 7, 207, 2, 208, 7, 208, 2, 209, 7, 209, 2, 210, 7, 210, 2, 211, 7, 211, 2, 212, 7, 212, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 4, 24, 654, 8, 24, 11, 24, 12, 24, 655, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 5, 25, 664, 8, 25, 10, 25, 12, 25, 667, 9, 25, 1, 25, 3, 25, 670, 8, 25, 1, 25, 3, 25, 673, 8, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 5, 26, 682, 8, 26, 10, 26, 12, 26, 685, 9, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 4, 27, 693, 8, 27, 11, 27, 12, 27, 694, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 33, 1, 33, 3, 33, 714, 8, 33, 1, 33, 4, 33, 717, 8, 33, 11, 33, 12, 33, 718, 1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 3, 36, 728, 8, 36, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 3, 38, 735, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 740, 8, 39, 10, 39, 12, 39, 743, 9, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 5, 39, 751, 8, 39, 10, 39, 12, 39, 754, 9, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 761, 8, 39, 1, 39, 3, 39, 764, 8, 39, 3, 39, 766, 8, 39, 1, 40, 4, 40, 769, 8, 40, 11, 40, 12, 40, 770, 1, 41, 4, 41, 774, 8, 41, 11, 41, 12, 41, 775, 1, 41, 1, 41, 5, 41, 780, 8, 41, 10, 41, 12, 41, 783, 9, 41, 1, 41, 1, 41, 4, 41, 787, 8, 41, 11, 41, 12, 41, 788, 1, 41, 4, 41, 792, 8, 41, 11, 41, 12, 41, 793, 1, 41, 1, 41, 5, 41, 798, 8, 41, 10, 41, 12, 41, 801, 9, 41, 3, 41, 803, 8, 41, 1, 41, 1, 41, 1, 41, 1, 41, 4, 41, 809, 8, 41, 11, 41, 12, 41, 810, 1, 41, 1, 41, 3, 41, 815, 8, 41, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 1, 75, 1, 75, 1, 76, 1, 76, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 3, 79, 943, 8, 79, 1, 79, 5, 79, 946, 8, 79, 10, 79, 12, 79, 949, 9, 79, 1, 79, 1, 79, 4, 79, 953, 8, 79, 11, 79, 12, 79, 954, 3, 79, 957, 8, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 5, 82, 971, 8, 82, 10, 82, 12, 82, 974, 9, 82, 1, 82, 1, 82, 3, 82, 978, 8, 82, 1, 82, 4, 82, 981, 8, 82, 11, 82, 12, 82, 982, 3, 82, 985, 8, 82, 1, 83, 1, 83, 4, 83, 989, 8, 83, 11, 83, 12, 83, 990, 1, 83, 1, 83, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 1, 90, 1, 91, 1, 91, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 1, 92, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 94, 1, 94, 1, 94, 1, 94, 1, 95, 1, 95, 1, 95, 1, 95, 1, 96, 1, 96, 1, 96, 1, 96, 1, 97, 1, 97, 1, 97, 1, 97, 1, 98, 1, 98, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 3, 100, 1068, 8, 100, 1, 101, 4, 101, 1071, 8, 101, 11, 101, 12, 101, 1072, 1, 102, 1, 102, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 108, 1, 109, 1, 109, 1, 109, 1, 109, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 3, 112, 1122, 8, 112, 1, 113, 1, 113, 3, 113, 1126, 8, 113, 1, 113, 5, 113, 1129, 8, 113, 10, 113, 12, 113, 1132, 9, 113, 1, 113, 1, 113, 3, 113, 1136, 8, 113, 1, 113, 4, 113, 1139, 8, 113, 11, 113, 12, 113, 1140, 3, 113, 1143, 8, 113, 1, 114, 1, 114, 4, 114, 1147, 8, 114, 11, 114, 12, 114, 1148, 1, 115, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 127, 1, 127, 1, 128, 1, 128, 1, 128, 1, 128, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 133, 1, 133, 1, 134, 4, 134, 1234, 8, 134, 11, 134, 12, 134, 1235, 1, 134, 1, 134, 3, 134, 1240, 8, 134, 1, 134, 4, 134, 1243, 8, 134, 11, 134, 12, 134, 1244, 1, 135, 1, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 1, 136, 1, 137, 1, 137, 1, 137, 1, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 140, 1, 140, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 1, 141, 1, 142, 1, 142, 1, 142, 1, 142, 1, 143, 1, 143, 1, 143, 1, 143, 1, 144, 1, 144, 1, 144, 1, 144, 1, 145, 1, 145, 1, 145, 1, 145, 1, 146, 1, 146, 1, 146, 1, 146, 1, 146, 1, 147, 1, 147, 1, 147, 1, 147, 1, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 1, 150, 1, 151, 1, 151, 1, 151, 1, 151, 1, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 1, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 156, 1, 156, 1, 156, 1, 156, 1, 157, 1, 157, 1, 157, 1, 157, 1, 158, 1, 158, 1, 158, 1, 158, 1, 159, 1, 159, 1, 159, 1, 159, 1, 160, 1, 160, 1, 160, 1, 160, 1, 160, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 1, 162, 1, 162, 1, 163, 1, 163, 1, 163, 1, 163, 1, 164, 1, 164, 1, 164, 1, 164, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 166, 1, 166, 1, 166, 1, 166, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 4, 167, 1390, 8, 167, 11, 167, 12, 167, 1391, 1, 168, 1, 168, 1, 168, 1, 168, 1, 169, 1, 169, 1, 169, 1, 169, 1, 170, 1, 170, 1, 170, 1, 170, 1, 171, 1, 171, 1, 171, 1, 171, 1, 171, 1, 172, 1, 172, 1, 172, 1, 172, 1, 173, 1, 173, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 1, 174, 1, 175, 1, 175, 1, 175, 1, 175, 1, 175, 1, 176, 1, 176, 1, 176, 1, 176, 1, 177, 1, 177, 1, 177, 1, 177, 1, 178, 1, 178, 1, 178, 1, 178, 1, 179, 1, 179, 1, 179, 1, 179, 1, 180, 1, 180, 1, 180, 1, 180, 1, 181, 1, 181, 1, 181, 1, 181, 1, 181, 1, 181, 1, 182, 1, 182, 1, 182, 1, 182, 1, 183, 1, 183, 1, 183, 1, 183, 1, 184, 1, 184, 1, 184, 1, 184, 1, 185, 1, 185, 1, 185, 1, 185, 1, 186, 1, 186, 1, 186, 1, 186, 1, 187, 1, 187, 1, 187, 1, 187, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 189, 1, 189, 1, 189, 1, 189, 1, 190, 1, 190, 1, 190, 1, 190, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 193, 1, 193, 1, 193, 1, 193, 1, 194, 1, 194, 1, 194, 1, 194, 1, 195, 1, 195, 1, 195, 1, 195, 1, 196, 1, 196, 1, 196, 1, 196, 1, 197, 1, 197, 1, 197, 1, 197, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 201, 1, 201, 1, 201, 1, 201, 1, 202, 1, 202, 1, 202, 1, 202, 1, 203, 1, 203, 1, 203, 1, 203, 1, 204, 1, 204, 1, 204, 1, 204, 1, 204, 1, 204, 1, 205, 1, 205, 1, 205, 1, 205, 1, 205, 1, 205, 1, 206, 1, 206, 1, 206, 1, 206, 1, 207, 1, 207, 1, 207, 1, 207, 1, 208, 1, 208, 1, 208, 1, 208, 1, 209, 1, 209, 1, 209, 1, 209, 1, 209, 1, 209, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 212, 1, 212, 1, 212, 1, 212, 1, 212, 2, 683, 752, 0, 213, 16, 1, 18, 2, 20, 3, 22, 4, 24, 5, 26, 6, 28, 7, 30, 8, 32, 9, 34, 10, 36, 11, 38, 12, 40, 13, 42, 14, 44, 15, 46, 16, 48, 17, 50, 18, 52, 19, 54, 20, 56, 21, 58, 22, 60, 23, 62, 24, 64, 25, 66, 26, 68, 27, 70, 28, 72, 29, 74, 0, 76, 0, 78, 0, 80, 0, 82, 0, 84, 0, 86, 0, 88, 0, 90, 0, 92, 0, 94, 30, 96, 31, 98, 32, 100, 33, 102, 34, 104, 35, 106, 36, 108, 37, 110, 38, 112, 39, 114, 40, 116, 41, 118, 42, 120, 43, 122, 44, 124, 45, 126, 46, 128, 47, 130, 48, 132, 49, 134, 50, 136, 51, 138, 52, 140, 53, 142, 54, 144, 55, 146, 56, 148, 57, 150, 58, 152, 59, 154, 60, 156, 61, 158, 62, 160, 63, 162, 64, 164, 65, 166, 66, 168, 67, 170, 68, 172, 0, 174, 69, 176, 70, 178, 71, 180, 72, 182, 0, 184, 73, 186, 74, 188, 75, 190, 76, 192, 0, 194, 0, 196, 77, 198, 78, 200, 79, 202, 0, 204, 0, 206, 0, 208, 0, 210, 0, 212, 0, 214, 80, 216, 0, 218, 81, 220, 0, 222, 0, 224, 82, 226, 83, 228, 84, 230, 0, 232, 0, 234, 0, 236, 0, 238, 0, 240, 0, 242, 0, 244, 85, 246, 86, 248, 87, 250, 88, 252, 0, 254, 0, 256, 0, 258, 0, 260, 0, 262, 0, 264, 89, 266, 0, 268, 90, 270, 91, 272, 92, 274, 0, 276, 0, 278, 93, 280, 94, 282, 0, 284, 95, 286, 0, 288, 96, 290, 97, 292, 98, 294, 0, 296, 0, 298, 0, 300, 0, 302, 0, 304, 0, 306, 0, 308, 0, 310, 0, 312, 99, 314, 100, 316, 101, 318, 0, 320, 0, 322, 0, 324, 0, 326, 0, 328, 0, 330, 102, 332, 103, 334, 104, 336, 0, 338, 105, 340, 106, 342, 107, 344, 108, 346, 0, 348, 0, 350, 109, 352, 110, 354, 111, 356, 112, 358, 0, 360, 0, 362, 0, 364, 0, 366, 0, 368, 0, 370, 0, 372, 113, 374, 114, 376, 115, 378, 0, 380, 0, 382, 0, 384, 0, 386, 116, 388, 117, 390, 118, 392, 0, 394, 0, 396, 0, 398, 0, 400, 119, 402, 0, 404, 0, 406, 120, 408, 121, 410, 122, 412, 0, 414, 0, 416, 0, 418, 123, 420, 124, 422, 125, 424, 0, 426, 0, 428, 126, 430, 127, 432, 128, 434, 0, 436, 0, 438, 0, 440, 0, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 36, 2, 0, 68, 68, 100, 100, 2, 0, 73, 73, 105, 105, 2, 0, 83, 83, 115, 115, 2, 0, 69, 69, 101, 101, 2, 0, 67, 67, 99, 99, 2, 0, 84, 84, 116, 116, 2, 0, 82, 82, 114, 114, 2, 0, 79, 79, 111, 111, 2, 0, 80, 80, 112, 112, 2, 0, 78, 78, 110, 110, 2, 0, 72, 72, 104, 104, 2, 0, 86, 86, 118, 118, 2, 0, 65, 65, 97, 97, 2, 0, 76, 76, 108, 108, 2, 0, 88, 88, 120, 120, 2, 0, 70, 70, 102, 102, 2, 0, 77, 77, 109, 109, 2, 0, 71, 71, 103, 103, 2, 0, 75, 75, 107, 107, 2, 0, 87, 87, 119, 119, 2, 0, 85, 85, 117, 117, 2, 0, 74, 74, 106, 106, 6, 0, 9, 10, 13, 13, 32, 32, 47, 47, 91, 91, 93, 93, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 2, 0, 65, 90, 97, 122, 8, 0, 34, 34, 78, 78, 82, 82, 84, 84, 92, 92, 110, 110, 114, 114, 116, 116, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 43, 43, 45, 45, 1, 0, 96, 96, 2, 0, 66, 66, 98, 98, 2, 0, 89, 89, 121, 121, 11, 0, 9, 10, 13, 13, 32, 32, 34, 34, 44, 44, 47, 47, 58, 58, 61, 61, 91, 91, 93, 93, 124, 124, 2, 0, 42, 42, 47, 47, 11, 0, 9, 10, 13, 13, 32, 32, 34, 35, 44, 44, 47, 47, 58, 58, 60, 60, 62, 63, 92, 92, 124, 124, 1628, 0, 16, 1, 0, 0, 0, 0, 18, 1, 0, 0, 0, 0, 20, 1, 0, 0, 0, 0, 22, 1, 0, 0, 0, 0, 24, 1, 0, 0, 0, 0, 26, 1, 0, 0, 0, 0, 28, 1, 0, 0, 0, 0, 30, 1, 0, 0, 0, 0, 32, 1, 0, 0, 0, 0, 34, 1, 0, 0, 0, 0, 36, 1, 0, 0, 0, 0, 38, 1, 0, 0, 0, 0, 40, 1, 0, 0, 0, 0, 42, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 46, 1, 0, 0, 0, 0, 48, 1, 0, 0, 0, 0, 50, 1, 0, 0, 0, 0, 52, 1, 0, 0, 0, 0, 54, 1, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 58, 1, 0, 0, 0, 0, 60, 1, 0, 0, 0, 0, 62, 1, 0, 0, 0, 0, 64, 1, 0, 0, 0, 0, 66, 1, 0, 0, 0, 0, 68, 1, 0, 0, 0, 0, 70, 1, 0, 0, 0, 1, 72, 1, 0, 0, 0, 1, 94, 1, 0, 0, 0, 1, 96, 1, 0, 0, 0, 1, 98, 1, 0, 0, 0, 1, 100, 1, 0, 0, 0, 1, 102, 1, 0, 0, 0, 1, 104, 1, 0, 0, 0, 1, 106, 1, 0, 0, 0, 1, 108, 1, 0, 0, 0, 1, 110, 1, 0, 0, 0, 1, 112, 1, 0, 0, 0, 1, 114, 1, 0, 0, 0, 1, 116, 1, 0, 0, 0, 1, 118, 1, 0, 0, 0, 1, 120, 1, 0, 0, 0, 1, 122, 1, 0, 0, 0, 1, 124, 1, 0, 0, 0, 1, 126, 1, 0, 0, 0, 1, 128, 1, 0, 0, 0, 1, 130, 1, 0, 0, 0, 1, 132, 1, 0, 0, 0, 1, 134, 1, 0, 0, 0, 1, 136, 1, 0, 0, 0, 1, 138, 1, 0, 0, 0, 1, 140, 1, 0, 0, 0, 1, 142, 1, 0, 0, 0, 1, 144, 1, 0, 0, 0, 1, 146, 1, 0, 0, 0, 1, 148, 1, 0, 0, 0, 1, 150, 1, 0, 0, 0, 1, 152, 1, 0, 0, 0, 1, 154, 1, 0, 0, 0, 1, 156, 1, 0, 0, 0, 1, 158, 1, 0, 0, 0, 1, 160, 1, 0, 0, 0, 1, 162, 1, 0, 0, 0, 1, 164, 1, 0, 0, 0, 1, 166, 1, 0, 0, 0, 1, 168, 1, 0, 0, 0, 1, 170, 1, 0, 0, 0, 1, 172, 1, 0, 0, 0, 1, 174, 1, 0, 0, 0, 1, 176, 1, 0, 0, 0, 1, 178, 1, 0, 0, 0, 1, 180, 1, 0, 0, 0, 1, 184, 1, 0, 0, 0, 1, 186, 1, 0, 0, 0, 1, 188, 1, 0, 0, 0, 1, 190, 1, 0, 0, 0, 2, 192, 1, 0, 0, 0, 2, 194, 1, 0, 0, 0, 2, 196, 1, 0, 0, 0, 2, 198, 1, 0, 0, 0, 2, 200, 1, 0, 0, 0, 3, 202, 1, 0, 0, 0, 3, 204, 1, 0, 0, 0, 3, 206, 1, 0, 0, 0, 3, 208, 1, 0, 0, 0, 3, 210, 1, 0, 0, 0, 3, 212, 1, 0, 0, 0, 3, 214, 1, 0, 0, 0, 3, 218, 1, 0, 0, 0, 3, 220, 1, 0, 0, 0, 3, 222, 1, 0, 0, 0, 3, 224, 1, 0, 0, 0, 3, 226, 1, 0, 0, 0, 3, 228, 1, 0, 0, 0, 4, 230, 1, 0, 0, 0, 4, 232, 1, 0, 0, 0, 4, 234, 1, 0, 0, 0, 4, 236, 1, 0, 0, 0, 4, 238, 1, 0, 0, 0, 4, 244, 1, 0, 0, 0, 4, 246, 1, 0, 0, 0, 4, 248, 1, 0, 0, 0, 4, 250, 1, 0, 0, 0, 5, 252, 1, 0, 0, 0, 5, 254, 1, 0, 0, 0, 5, 256, 1, 0, 0, 0, 5, 258, 1, 0, 0, 0, 5, 260, 1, 0, 0, 0, 5, 262, 1, 0, 0, 0, 5, 264, 1, 0, 0, 0, 5, 266, 1, 0, 0, 0, 5, 268, 1, 0, 0, 0, 5, 270, 1, 0, 0, 0, 5, 272, 1, 0, 0, 0, 6, 274, 1, 0, 0, 0, 6, 276, 1, 0, 0, 0, 6, 278, 1, 0, 0, 0, 6, 280, 1, 0, 0, 0, 6, 284, 1, 0, 0, 0, 6, 286, 1, 0, 0, 0, 6, 288, 1, 0, 0, 0, 6, 290, 1, 0, 0, 0, 6, 292, 1, 0, 0, 0, 7, 294, 1, 0, 0, 0, 7, 296, 1, 0, 0, 0, 7, 298, 1, 0, 0, 0, 7, 300, 1, 0, 0, 0, 7, 302, 1, 0, 0, 0, 7, 304, 1, 0, 0, 0, 7, 306, 1, 0, 0, 0, 7, 308, 1, 0, 0, 0, 7, 310, 1, 0, 0, 0, 7, 312, 1, 0, 0, 0, 7, 314, 1, 0, 0, 0, 7, 316, 1, 0, 0, 0, 8, 318, 1, 0, 0, 0, 8, 320, 1, 0, 0, 0, 8, 322, 1, 0, 0, 0, 8, 324, 1, 0, 0, 0, 8, 326, 1, 0, 0, 0, 8, 328, 1, 0, 0, 0, 8, 330, 1, 0, 0, 0, 8, 332, 1, 0, 0, 0, 8, 334, 1, 0, 0, 0, 9, 336, 1, 0, 0, 0, 9, 338, 1, 0, 0, 0, 9, 340, 1, 0, 0, 0, 9, 342, 1, 0, 0, 0, 9, 344, 1, 0, 0, 0, 10, 346, 1, 0, 0, 0, 10, 348, 1, 0, 0, 0, 10, 350, 1, 0, 0, 0, 10, 352, 1, 0, 0, 0, 10, 354, 1, 0, 0, 0, 10, 356, 1, 0, 0, 0, 11, 358, 1, 0, 0, 0, 11, 360, 1, 0, 0, 0, 11, 362, 1, 0, 0, 0, 11, 364, 1, 0, 0, 0, 11, 366, 1, 0, 0, 0, 11, 368, 1, 0, 0, 0, 11, 370, 1, 0, 0, 0, 11, 372, 1, 0, 0, 0, 11, 374, 1, 0, 0, 0, 11, 376, 1, 0, 0, 0, 12, 378, 1, 0, 0, 0, 12, 380, 1, 0, 0, 0, 12, 382, 1, 0, 0, 0, 12, 384, 1, 0, 0, 0, 12, 386, 1, 0, 0, 0, 12, 388, 1, 0, 0, 0, 12, 390, 1, 0, 0, 0, 13, 392, 1, 0, 0, 0, 13, 394, 1, 0, 0, 0, 13, 396, 1, 0, 0, 0, 13, 398, 1, 0, 0, 0, 13, 400, 1, 0, 0, 0, 13, 402, 1, 0, 0, 0, 13, 404, 1, 0, 0, 0, 13, 406, 1, 0, 0, 0, 13, 408, 1, 0, 0, 0, 13, 410, 1, 0, 0, 0, 14, 412, 1, 0, 0, 0, 14, 414, 1, 0, 0, 0, 14, 416, 1, 0, 0, 0, 14, 418, 1, 0, 0, 0, 14, 420, 1, 0, 0, 0, 14, 422, 1, 0, 0, 0, 15, 424, 1, 0, 0, 0, 15, 426, 1, 0, 0, 0, 15, 428, 1, 0, 0, 0, 15, 430, 1, 0, 0, 0, 15, 432, 1, 0, 0, 0, 15, 434, 1, 0, 0, 0, 15, 436, 1, 0, 0, 0, 15, 438, 1, 0, 0, 0, 15, 440, 1, 0, 0, 0, 16, 442, 1, 0, 0, 0, 18, 452, 1, 0, 0, 0, 20, 459, 1, 0, 0, 0, 22, 468, 1, 0, 0, 0, 24, 475, 1, 0, 0, 0, 26, 485, 1, 0, 0, 0, 28, 492, 1, 0, 0, 0, 30, 499, 1, 0, 0, 0, 32, 506, 1, 0, 0, 0, 34, 514, 1, 0, 0, 0, 36, 526, 1, 0, 0, 0, 38, 535, 1, 0, 0, 0, 40, 541, 1, 0, 0, 0, 42, 548, 1, 0, 0, 0, 44, 555, 1, 0, 0, 0, 46, 563, 1, 0, 0, 0, 48, 571, 1, 0, 0, 0, 50, 586, 1, 0, 0, 0, 52, 598, 1, 0, 0, 0, 54, 609, 1, 0, 0, 0, 56, 617, 1, 0, 0, 0, 58, 625, 1, 0, 0, 0, 60, 633, 1, 0, 0, 0, 62, 642, 1, 0, 0, 0, 64, 653, 1, 0, 0, 0, 66, 659, 1, 0, 0, 0, 68, 676, 1, 0, 0, 0, 70, 692, 1, 0, 0, 0, 72, 698, 1, 0, 0, 0, 74, 702, 1, 0, 0, 0, 76, 704, 1, 0, 0, 0, 78, 706, 1, 0, 0, 0, 80, 709, 1, 0, 0, 0, 82, 711, 1, 0, 0, 0, 84, 720, 1, 0, 0, 0, 86, 722, 1, 0, 0, 0, 88, 727, 1, 0, 0, 0, 90, 729, 1, 0, 0, 0, 92, 734, 1, 0, 0, 0, 94, 765, 1, 0, 0, 0, 96, 768, 1, 0, 0, 0, 98, 814, 1, 0, 0, 0, 100, 816, 1, 0, 0, 0, 102, 819, 1, 0, 0, 0, 104, 823, 1, 0, 0, 0, 106, 827, 1, 0, 0, 0, 108, 829, 1, 0, 0, 0, 110, 832, 1, 0, 0, 0, 112, 834, 1, 0, 0, 0, 114, 836, 1, 0, 0, 0, 116, 841, 1, 0, 0, 0, 118, 843, 1, 0, 0, 0, 120, 849, 1, 0, 0, 0, 122, 855, 1, 0, 0, 0, 124, 858, 1, 0, 0, 0, 126, 861, 1, 0, 0, 0, 128, 866, 1, 0, 0, 0, 130, 871, 1, 0, 0, 0, 132, 873, 1, 0, 0, 0, 134, 877, 1, 0, 0, 0, 136, 882, 1, 0, 0, 0, 138, 888, 1, 0, 0, 0, 140, 891, 1, 0, 0, 0, 142, 893, 1, 0, 0, 0, 144, 899, 1, 0, 0, 0, 146, 901, 1, 0, 0, 0, 148, 906, 1, 0, 0, 0, 150, 909, 1, 0, 0, 0, 152, 912, 1, 0, 0, 0, 154, 915, 1, 0, 0, 0, 156, 917, 1, 0, 0, 0, 158, 920, 1, 0, 0, 0, 160, 922, 1, 0, 0, 0, 162, 925, 1, 0, 0, 0, 164, 927, 1, 0, 0, 0, 166, 929, 1, 0, 0, 0, 168, 931, 1, 0, 0, 0, 170, 933, 1, 0, 0, 0, 172, 935, 1, 0, 0, 0, 174, 956, 1, 0, 0, 0, 176, 958, 1, 0, 0, 0, 178, 963, 1, 0, 0, 0, 180, 984, 1, 0, 0, 0, 182, 986, 1, 0, 0, 0, 184, 994, 1, 0, 0, 0, 186, 996, 1, 0, 0, 0, 188, 1000, 1, 0, 0, 0, 190, 1004, 1, 0, 0, 0, 192, 1008, 1, 0, 0, 0, 194, 1013, 1, 0, 0, 0, 196, 1018, 1, 0, 0, 0, 198, 1022, 1, 0, 0, 0, 200, 1026, 1, 0, 0, 0, 202, 1030, 1, 0, 0, 0, 204, 1035, 1, 0, 0, 0, 206, 1039, 1, 0, 0, 0, 208, 1043, 1, 0, 0, 0, 210, 1047, 1, 0, 0, 0, 212, 1051, 1, 0, 0, 0, 214, 1055, 1, 0, 0, 0, 216, 1067, 1, 0, 0, 0, 218, 1070, 1, 0, 0, 0, 220, 1074, 1, 0, 0, 0, 222, 1078, 1, 0, 0, 0, 224, 1082, 1, 0, 0, 0, 226, 1086, 1, 0, 0, 0, 228, 1090, 1, 0, 0, 0, 230, 1094, 1, 0, 0, 0, 232, 1099, 1, 0, 0, 0, 234, 1103, 1, 0, 0, 0, 236, 1107, 1, 0, 0, 0, 238, 1112, 1, 0, 0, 0, 240, 1121, 1, 0, 0, 0, 242, 1142, 1, 0, 0, 0, 244, 1146, 1, 0, 0, 0, 246, 1150, 1, 0, 0, 0, 248, 1154, 1, 0, 0, 0, 250, 1158, 1, 0, 0, 0, 252, 1162, 1, 0, 0, 0, 254, 1167, 1, 0, 0, 0, 256, 1171, 1, 0, 0, 0, 258, 1175, 1, 0, 0, 0, 260, 1179, 1, 0, 0, 0, 262, 1184, 1, 0, 0, 0, 264, 1189, 1, 0, 0, 0, 266, 1192, 1, 0, 0, 0, 268, 1196, 1, 0, 0, 0, 270, 1200, 1, 0, 0, 0, 272, 1204, 1, 0, 0, 0, 274, 1208, 1, 0, 0, 0, 276, 1213, 1, 0, 0, 0, 278, 1218, 1, 0, 0, 0, 280, 1223, 1, 0, 0, 0, 282, 1230, 1, 0, 0, 0, 284, 1239, 1, 0, 0, 0, 286, 1246, 1, 0, 0, 0, 288, 1250, 1, 0, 0, 0, 290, 1254, 1, 0, 0, 0, 292, 1258, 1, 0, 0, 0, 294, 1262, 1, 0, 0, 0, 296, 1268, 1, 0, 0, 0, 298, 1272, 1, 0, 0, 0, 300, 1276, 1, 0, 0, 0, 302, 1280, 1, 0, 0, 0, 304, 1284, 1, 0, 0, 0, 306, 1288, 1, 0, 0, 0, 308, 1292, 1, 0, 0, 0, 310, 1297, 1, 0, 0, 0, 312, 1302, 1, 0, 0, 0, 314, 1306, 1, 0, 0, 0, 316, 1310, 1, 0, 0, 0, 318, 1314, 1, 0, 0, 0, 320, 1319, 1, 0, 0, 0, 322, 1323, 1, 0, 0, 0, 324, 1328, 1, 0, 0, 0, 326, 1333, 1, 0, 0, 0, 328, 1337, 1, 0, 0, 0, 330, 1341, 1, 0, 0, 0, 332, 1345, 1, 0, 0, 0, 334, 1349, 1, 0, 0, 0, 336, 1353, 1, 0, 0, 0, 338, 1358, 1, 0, 0, 0, 340, 1363, 1, 0, 0, 0, 342, 1367, 1, 0, 0, 0, 344, 1371, 1, 0, 0, 0, 346, 1375, 1, 0, 0, 0, 348, 1380, 1, 0, 0, 0, 350, 1389, 1, 0, 0, 0, 352, 1393, 1, 0, 0, 0, 354, 1397, 1, 0, 0, 0, 356, 1401, 1, 0, 0, 0, 358, 1405, 1, 0, 0, 0, 360, 1410, 1, 0, 0, 0, 362, 1414, 1, 0, 0, 0, 364, 1418, 1, 0, 0, 0, 366, 1422, 1, 0, 0, 0, 368, 1427, 1, 0, 0, 0, 370, 1431, 1, 0, 0, 0, 372, 1435, 1, 0, 0, 0, 374, 1439, 1, 0, 0, 0, 376, 1443, 1, 0, 0, 0, 378, 1447, 1, 0, 0, 0, 380, 1453, 1, 0, 0, 0, 382, 1457, 1, 0, 0, 0, 384, 1461, 1, 0, 0, 0, 386, 1465, 1, 0, 0, 0, 388, 1469, 1, 0, 0, 0, 390, 1473, 1, 0, 0, 0, 392, 1477, 1, 0, 0, 0, 394, 1482, 1, 0, 0, 0, 396, 1486, 1, 0, 0, 0, 398, 1490, 1, 0, 0, 0, 400, 1496, 1, 0, 0, 0, 402, 1505, 1, 0, 0, 0, 404, 1509, 1, 0, 0, 0, 406, 1513, 1, 0, 0, 0, 408, 1517, 1, 0, 0, 0, 410, 1521, 1, 0, 0, 0, 412, 1525, 1, 0, 0, 0, 414, 1530, 1, 0, 0, 0, 416, 1536, 1, 0, 0, 0, 418, 1542, 1, 0, 0, 0, 420, 1546, 1, 0, 0, 0, 422, 1550, 1, 0, 0, 0, 424, 1554, 1, 0, 0, 0, 426, 1560, 1, 0, 0, 0, 428, 1566, 1, 0, 0, 0, 430, 1570, 1, 0, 0, 0, 432, 1574, 1, 0, 0, 0, 434, 1578, 1, 0, 0, 0, 436, 1584, 1, 0, 0, 0, 438, 1590, 1, 0, 0, 0, 440, 1596, 1, 0, 0, 0, 442, 443, 7, 0, 0, 0, 443, 444, 7, 1, 0, 0, 444, 445, 7, 2, 0, 0, 445, 446, 7, 2, 0, 0, 446, 447, 7, 3, 0, 0, 447, 448, 7, 4, 0, 0, 448, 449, 7, 5, 0, 0, 449, 450, 1, 0, 0, 0, 450, 451, 6, 0, 0, 0, 451, 17, 1, 0, 0, 0, 452, 453, 7, 0, 0, 0, 453, 454, 7, 6, 0, 0, 454, 455, 7, 7, 0, 0, 455, 456, 7, 8, 0, 0, 456, 457, 1, 0, 0, 0, 457, 458, 6, 1, 1, 0, 458, 19, 1, 0, 0, 0, 459, 460, 7, 3, 0, 0, 460, 461, 7, 9, 0, 0, 461, 462, 7, 6, 0, 0, 462, 463, 7, 1, 0, 0, 463, 464, 7, 4, 0, 0, 464, 465, 7, 10, 0, 0, 465, 466, 1, 0, 0, 0, 466, 467, 6, 2, 2, 0, 467, 21, 1, 0, 0, 0, 468, 469, 7, 3, 0, 0, 469, 470, 7, 11, 0, 0, 470, 471, 7, 12, 0, 0, 471, 472, 7, 13, 0, 0, 472, 473, 1, 0, 0, 0, 473, 474, 6, 3, 0, 0, 474, 23, 1, 0, 0, 0, 475, 476, 7, 3, 0, 0, 476, 477, 7, 14, 0, 0, 477, 478, 7, 8, 0, 0, 478, 479, 7, 13, 0, 0, 479, 480, 7, 12, 0, 0, 480, 481, 7, 1, 0, 0, 481, 482, 7, 9, 0, 0, 482, 483, 1, 0, 0, 0, 483, 484, 6, 4, 3, 0, 484, 25, 1, 0, 0, 0, 485, 486, 7, 15, 0, 0, 486, 487, 7, 6, 0, 0, 487, 488, 7, 7, 0, 0, 488, 489, 7, 16, 0, 0, 489, 490, 1, 0, 0, 0, 490, 491, 6, 5, 4, 0, 491, 27, 1, 0, 0, 0, 492, 493, 7, 17, 0, 0, 493, 494, 7, 6, 0, 0, 494, 495, 7, 7, 0, 0, 495, 496, 7, 18, 0, 0, 496, 497, 1, 0, 0, 0, 497, 498, 6, 6, 0, 0, 498, 29, 1, 0, 0, 0, 499, 500, 7, 18, 0, 0, 500, 501, 7, 3, 0, 0, 501, 502, 7, 3, 0, 0, 502, 503, 7, 8, 0, 0, 503, 504, 1, 0, 0, 0, 504, 505, 6, 7, 1, 0, 505, 31, 1, 0, 0, 0, 506, 507, 7, 13, 0, 0, 507, 508, 7, 1, 0, 0, 508, 509, 7, 16, 0, 0, 509, 510, 7, 1, 0, 0, 510, 511, 7, 5, 0, 0, 511, 512, 1, 0, 0, 0, 512, 513, 6, 8, 0, 0, 513, 33, 1, 0, 0, 0, 514, 515, 7, 16, 0, 0, 515, 516, 7, 11, 0, 0, 516, 517, 5, 95, 0, 0, 517, 518, 7, 3, 0, 0, 518, 519, 7, 14, 0, 0, 519, 520, 7, 8, 0, 0, 520, 521, 7, 12, 0, 0, 521, 522, 7, 9, 0, 0, 522, 523, 7, 0, 0, 0, 523, 524, 1, 0, 0, 0, 524, 525, 6, 9, 5, 0, 525, 35, 1, 0, 0, 0, 526, 527, 7, 6, 0, 0, 527, 528, 7, 3, 0, 0, 528, 529, 7, 9, 0, 0, 529, 530, 7, 12, 0, 0, 530, 531, 7, 16, 0, 0, 531, 532, 7, 3, 0, 0, 532, 533, 1, 0, 0, 0, 533, 534, 6, 10, 6, 0, 534, 37, 1, 0, 0, 0, 535, 536, 7, 6, 0, 0, 536, 537, 7, 7, 0, 0, 537, 538, 7, 19, 0, 0, 538, 539, 1, 0, 0, 0, 539, 540, 6, 11, 0, 0, 540, 39, 1, 0, 0, 0, 541, 542, 7, 2, 0, 0, 542, 543, 7, 10, 0, 0, 543, 544, 7, 7, 0, 0, 544, 545, 7, 19, 0, 0, 545, 546, 1, 0, 0, 0, 546, 547, 6, 12, 7, 0, 547, 41, 1, 0, 0, 0, 548, 549, 7, 2, 0, 0, 549, 550, 7, 7, 0, 0, 550, 551, 7, 6, 0, 0, 551, 552, 7, 5, 0, 0, 552, 553, 1, 0, 0, 0, 553, 554, 6, 13, 0, 0, 554, 43, 1, 0, 0, 0, 555, 556, 7, 2, 0, 0, 556, 557, 7, 5, 0, 0, 557, 558, 7, 12, 0, 0, 558, 559, 7, 5, 0, 0, 559, 560, 7, 2, 0, 0, 560, 561, 1, 0, 0, 0, 561, 562, 6, 14, 0, 0, 562, 45, 1, 0, 0, 0, 563, 564, 7, 19, 0, 0, 564, 565, 7, 10, 0, 0, 565, 566, 7, 3, 0, 0, 566, 567, 7, 6, 0, 0, 567, 568, 7, 3, 0, 0, 568, 569, 1, 0, 0, 0, 569, 570, 6, 15, 0, 0, 570, 47, 1, 0, 0, 0, 571, 572, 4, 16, 0, 0, 572, 573, 7, 1, 0, 0, 573, 574, 7, 9, 0, 0, 574, 575, 7, 13, 0, 0, 575, 576, 7, 1, 0, 0, 576, 577, 7, 9, 0, 0, 577, 578, 7, 3, 0, 0, 578, 579, 7, 2, 0, 0, 579, 580, 7, 5, 0, 0, 580, 581, 7, 12, 0, 0, 581, 582, 7, 5, 0, 0, 582, 583, 7, 2, 0, 0, 583, 584, 1, 0, 0, 0, 584, 585, 6, 16, 0, 0, 585, 49, 1, 0, 0, 0, 586, 587, 4, 17, 1, 0, 587, 588, 7, 13, 0, 0, 588, 589, 7, 7, 0, 0, 589, 590, 7, 7, 0, 0, 590, 591, 7, 18, 0, 0, 591, 592, 7, 20, 0, 0, 592, 593, 7, 8, 0, 0, 593, 594, 5, 95, 0, 0, 594, 595, 5, 128020, 0, 0, 595, 596, 1, 0, 0, 0, 596, 597, 6, 17, 8, 0, 597, 51, 1, 0, 0, 0, 598, 599, 4, 18, 2, 0, 599, 600, 7, 16, 0, 0, 600, 601, 7, 3, 0, 0, 601, 602, 7, 5, 0, 0, 602, 603, 7, 6, 0, 0, 603, 604, 7, 1, 0, 0, 604, 605, 7, 4, 0, 0, 605, 606, 7, 2, 0, 0, 606, 607, 1, 0, 0, 0, 607, 608, 6, 18, 9, 0, 608, 53, 1, 0, 0, 0, 609, 610, 4, 19, 3, 0, 610, 611, 7, 21, 0, 0, 611, 612, 7, 7, 0, 0, 612, 613, 7, 1, 0, 0, 613, 614, 7, 9, 0, 0, 614, 615, 1, 0, 0, 0, 615, 616, 6, 19, 10, 0, 616, 55, 1, 0, 0, 0, 617, 618, 4, 20, 4, 0, 618, 619, 7, 15, 0, 0, 619, 620, 7, 20, 0, 0, 620, 621, 7, 13, 0, 0, 621, 622, 7, 13, 0, 0, 622, 623, 1, 0, 0, 0, 623, 624, 6, 20, 10, 0, 624, 57, 1, 0, 0, 0, 625, 626, 4, 21, 5, 0, 626, 627, 7, 13, 0, 0, 627, 628, 7, 3, 0, 0, 628, 629, 7, 15, 0, 0, 629, 630, 7, 5, 0, 0, 630, 631, 1, 0, 0, 0, 631, 632, 6, 21, 10, 0, 632, 59, 1, 0, 0, 0, 633, 634, 4, 22, 6, 0, 634, 635, 7, 6, 0, 0, 635, 636, 7, 1, 0, 0, 636, 637, 7, 17, 0, 0, 637, 638, 7, 10, 0, 0, 638, 639, 7, 5, 0, 0, 639, 640, 1, 0, 0, 0, 640, 641, 6, 22, 10, 0, 641, 61, 1, 0, 0, 0, 642, 643, 4, 23, 7, 0, 643, 644, 7, 13, 0, 0, 644, 645, 7, 7, 0, 0, 645, 646, 7, 7, 0, 0, 646, 647, 7, 18, 0, 0, 647, 648, 7, 20, 0, 0, 648, 649, 7, 8, 0, 0, 649, 650, 1, 0, 0, 0, 650, 651, 6, 23, 10, 0, 651, 63, 1, 0, 0, 0, 652, 654, 8, 22, 0, 0, 653, 652, 1, 0, 0, 0, 654, 655, 1, 0, 0, 0, 655, 653, 1, 0, 0, 0, 655, 656, 1, 0, 0, 0, 656, 657, 1, 0, 0, 0, 657, 658, 6, 24, 0, 0, 658, 65, 1, 0, 0, 0, 659, 660, 5, 47, 0, 0, 660, 661, 5, 47, 0, 0, 661, 665, 1, 0, 0, 0, 662, 664, 8, 23, 0, 0, 663, 662, 1, 0, 0, 0, 664, 667, 1, 0, 0, 0, 665, 663, 1, 0, 0, 0, 665, 666, 1, 0, 0, 0, 666, 669, 1, 0, 0, 0, 667, 665, 1, 0, 0, 0, 668, 670, 5, 13, 0, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 672, 1, 0, 0, 0, 671, 673, 5, 10, 0, 0, 672, 671, 1, 0, 0, 0, 672, 673, 1, 0, 0, 0, 673, 674, 1, 0, 0, 0, 674, 675, 6, 25, 11, 0, 675, 67, 1, 0, 0, 0, 676, 677, 5, 47, 0, 0, 677, 678, 5, 42, 0, 0, 678, 683, 1, 0, 0, 0, 679, 682, 3, 68, 26, 0, 680, 682, 9, 0, 0, 0, 681, 679, 1, 0, 0, 0, 681, 680, 1, 0, 0, 0, 682, 685, 1, 0, 0, 0, 683, 684, 1, 0, 0, 0, 683, 681, 1, 0, 0, 0, 684, 686, 1, 0, 0, 0, 685, 683, 1, 0, 0, 0, 686, 687, 5, 42, 0, 0, 687, 688, 5, 47, 0, 0, 688, 689, 1, 0, 0, 0, 689, 690, 6, 26, 11, 0, 690, 69, 1, 0, 0, 0, 691, 693, 7, 24, 0, 0, 692, 691, 1, 0, 0, 0, 693, 694, 1, 0, 0, 0, 694, 692, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 697, 6, 27, 11, 0, 697, 71, 1, 0, 0, 0, 698, 699, 5, 124, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 6, 28, 12, 0, 701, 73, 1, 0, 0, 0, 702, 703, 7, 25, 0, 0, 703, 75, 1, 0, 0, 0, 704, 705, 7, 26, 0, 0, 705, 77, 1, 0, 0, 0, 706, 707, 5, 92, 0, 0, 707, 708, 7, 27, 0, 0, 708, 79, 1, 0, 0, 0, 709, 710, 8, 28, 0, 0, 710, 81, 1, 0, 0, 0, 711, 713, 7, 3, 0, 0, 712, 714, 7, 29, 0, 0, 713, 712, 1, 0, 0, 0, 713, 714, 1, 0, 0, 0, 714, 716, 1, 0, 0, 0, 715, 717, 3, 74, 29, 0, 716, 715, 1, 0, 0, 0, 717, 718, 1, 0, 0, 0, 718, 716, 1, 0, 0, 0, 718, 719, 1, 0, 0, 0, 719, 83, 1, 0, 0, 0, 720, 721, 5, 64, 0, 0, 721, 85, 1, 0, 0, 0, 722, 723, 5, 96, 0, 0, 723, 87, 1, 0, 0, 0, 724, 728, 8, 30, 0, 0, 725, 726, 5, 96, 0, 0, 726, 728, 5, 96, 0, 0, 727, 724, 1, 0, 0, 0, 727, 725, 1, 0, 0, 0, 728, 89, 1, 0, 0, 0, 729, 730, 5, 95, 0, 0, 730, 91, 1, 0, 0, 0, 731, 735, 3, 76, 30, 0, 732, 735, 3, 74, 29, 0, 733, 735, 3, 90, 37, 0, 734, 731, 1, 0, 0, 0, 734, 732, 1, 0, 0, 0, 734, 733, 1, 0, 0, 0, 735, 93, 1, 0, 0, 0, 736, 741, 5, 34, 0, 0, 737, 740, 3, 78, 31, 0, 738, 740, 3, 80, 32, 0, 739, 737, 1, 0, 0, 0, 739, 738, 1, 0, 0, 0, 740, 743, 1, 0, 0, 0, 741, 739, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 744, 1, 0, 0, 0, 743, 741, 1, 0, 0, 0, 744, 766, 5, 34, 0, 0, 745, 746, 5, 34, 0, 0, 746, 747, 5, 34, 0, 0, 747, 748, 5, 34, 0, 0, 748, 752, 1, 0, 0, 0, 749, 751, 8, 23, 0, 0, 750, 749, 1, 0, 0, 0, 751, 754, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 752, 750, 1, 0, 0, 0, 753, 755, 1, 0, 0, 0, 754, 752, 1, 0, 0, 0, 755, 756, 5, 34, 0, 0, 756, 757, 5, 34, 0, 0, 757, 758, 5, 34, 0, 0, 758, 760, 1, 0, 0, 0, 759, 761, 5, 34, 0, 0, 760, 759, 1, 0, 0, 0, 760, 761, 1, 0, 0, 0, 761, 763, 1, 0, 0, 0, 762, 764, 5, 34, 0, 0, 763, 762, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 766, 1, 0, 0, 0, 765, 736, 1, 0, 0, 0, 765, 745, 1, 0, 0, 0, 766, 95, 1, 0, 0, 0, 767, 769, 3, 74, 29, 0, 768, 767, 1, 0, 0, 0, 769, 770, 1, 0, 0, 0, 770, 768, 1, 0, 0, 0, 770, 771, 1, 0, 0, 0, 771, 97, 1, 0, 0, 0, 772, 774, 3, 74, 29, 0, 773, 772, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 773, 1, 0, 0, 0, 775, 776, 1, 0, 0, 0, 776, 777, 1, 0, 0, 0, 777, 781, 3, 116, 50, 0, 778, 780, 3, 74, 29, 0, 779, 778, 1, 0, 0, 0, 780, 783, 1, 0, 0, 0, 781, 779, 1, 0, 0, 0, 781, 782, 1, 0, 0, 0, 782, 815, 1, 0, 0, 0, 783, 781, 1, 0, 0, 0, 784, 786, 3, 116, 50, 0, 785, 787, 3, 74, 29, 0, 786, 785, 1, 0, 0, 0, 787, 788, 1, 0, 0, 0, 788, 786, 1, 0, 0, 0, 788, 789, 1, 0, 0, 0, 789, 815, 1, 0, 0, 0, 790, 792, 3, 74, 29, 0, 791, 790, 1, 0, 0, 0, 792, 793, 1, 0, 0, 0, 793, 791, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 802, 1, 0, 0, 0, 795, 799, 3, 116, 50, 0, 796, 798, 3, 74, 29, 0, 797, 796, 1, 0, 0, 0, 798, 801, 1, 0, 0, 0, 799, 797, 1, 0, 0, 0, 799, 800, 1, 0, 0, 0, 800, 803, 1, 0, 0, 0, 801, 799, 1, 0, 0, 0, 802, 795, 1, 0, 0, 0, 802, 803, 1, 0, 0, 0, 803, 804, 1, 0, 0, 0, 804, 805, 3, 82, 33, 0, 805, 815, 1, 0, 0, 0, 806, 808, 3, 116, 50, 0, 807, 809, 3, 74, 29, 0, 808, 807, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 808, 1, 0, 0, 0, 810, 811, 1, 0, 0, 0, 811, 812, 1, 0, 0, 0, 812, 813, 3, 82, 33, 0, 813, 815, 1, 0, 0, 0, 814, 773, 1, 0, 0, 0, 814, 784, 1, 0, 0, 0, 814, 791, 1, 0, 0, 0, 814, 806, 1, 0, 0, 0, 815, 99, 1, 0, 0, 0, 816, 817, 7, 31, 0, 0, 817, 818, 7, 32, 0, 0, 818, 101, 1, 0, 0, 0, 819, 820, 7, 12, 0, 0, 820, 821, 7, 9, 0, 0, 821, 822, 7, 0, 0, 0, 822, 103, 1, 0, 0, 0, 823, 824, 7, 12, 0, 0, 824, 825, 7, 2, 0, 0, 825, 826, 7, 4, 0, 0, 826, 105, 1, 0, 0, 0, 827, 828, 5, 61, 0, 0, 828, 107, 1, 0, 0, 0, 829, 830, 5, 58, 0, 0, 830, 831, 5, 58, 0, 0, 831, 109, 1, 0, 0, 0, 832, 833, 5, 58, 0, 0, 833, 111, 1, 0, 0, 0, 834, 835, 5, 44, 0, 0, 835, 113, 1, 0, 0, 0, 836, 837, 7, 0, 0, 0, 837, 838, 7, 3, 0, 0, 838, 839, 7, 2, 0, 0, 839, 840, 7, 4, 0, 0, 840, 115, 1, 0, 0, 0, 841, 842, 5, 46, 0, 0, 842, 117, 1, 0, 0, 0, 843, 844, 7, 15, 0, 0, 844, 845, 7, 12, 0, 0, 845, 846, 7, 13, 0, 0, 846, 847, 7, 2, 0, 0, 847, 848, 7, 3, 0, 0, 848, 119, 1, 0, 0, 0, 849, 850, 7, 15, 0, 0, 850, 851, 7, 1, 0, 0, 851, 852, 7, 6, 0, 0, 852, 853, 7, 2, 0, 0, 853, 854, 7, 5, 0, 0, 854, 121, 1, 0, 0, 0, 855, 856, 7, 1, 0, 0, 856, 857, 7, 9, 0, 0, 857, 123, 1, 0, 0, 0, 858, 859, 7, 1, 0, 0, 859, 860, 7, 2, 0, 0, 860, 125, 1, 0, 0, 0, 861, 862, 7, 13, 0, 0, 862, 863, 7, 12, 0, 0, 863, 864, 7, 2, 0, 0, 864, 865, 7, 5, 0, 0, 865, 127, 1, 0, 0, 0, 866, 867, 7, 13, 0, 0, 867, 868, 7, 1, 0, 0, 868, 869, 7, 18, 0, 0, 869, 870, 7, 3, 0, 0, 870, 129, 1, 0, 0, 0, 871, 872, 5, 40, 0, 0, 872, 131, 1, 0, 0, 0, 873, 874, 7, 9, 0, 0, 874, 875, 7, 7, 0, 0, 875, 876, 7, 5, 0, 0, 876, 133, 1, 0, 0, 0, 877, 878, 7, 9, 0, 0, 878, 879, 7, 20, 0, 0, 879, 880, 7, 13, 0, 0, 880, 881, 7, 13, 0, 0, 881, 135, 1, 0, 0, 0, 882, 883, 7, 9, 0, 0, 883, 884, 7, 20, 0, 0, 884, 885, 7, 13, 0, 0, 885, 886, 7, 13, 0, 0, 886, 887, 7, 2, 0, 0, 887, 137, 1, 0, 0, 0, 888, 889, 7, 7, 0, 0, 889, 890, 7, 6, 0, 0, 890, 139, 1, 0, 0, 0, 891, 892, 5, 63, 0, 0, 892, 141, 1, 0, 0, 0, 893, 894, 7, 6, 0, 0, 894, 895, 7, 13, 0, 0, 895, 896, 7, 1, 0, 0, 896, 897, 7, 18, 0, 0, 897, 898, 7, 3, 0, 0, 898, 143, 1, 0, 0, 0, 899, 900, 5, 41, 0, 0, 900, 145, 1, 0, 0, 0, 901, 902, 7, 5, 0, 0, 902, 903, 7, 6, 0, 0, 903, 904, 7, 20, 0, 0, 904, 905, 7, 3, 0, 0, 905, 147, 1, 0, 0, 0, 906, 907, 5, 61, 0, 0, 907, 908, 5, 61, 0, 0, 908, 149, 1, 0, 0, 0, 909, 910, 5, 61, 0, 0, 910, 911, 5, 126, 0, 0, 911, 151, 1, 0, 0, 0, 912, 913, 5, 33, 0, 0, 913, 914, 5, 61, 0, 0, 914, 153, 1, 0, 0, 0, 915, 916, 5, 60, 0, 0, 916, 155, 1, 0, 0, 0, 917, 918, 5, 60, 0, 0, 918, 919, 5, 61, 0, 0, 919, 157, 1, 0, 0, 0, 920, 921, 5, 62, 0, 0, 921, 159, 1, 0, 0, 0, 922, 923, 5, 62, 0, 0, 923, 924, 5, 61, 0, 0, 924, 161, 1, 0, 0, 0, 925, 926, 5, 43, 0, 0, 926, 163, 1, 0, 0, 0, 927, 928, 5, 45, 0, 0, 928, 165, 1, 0, 0, 0, 929, 930, 5, 42, 0, 0, 930, 167, 1, 0, 0, 0, 931, 932, 5, 47, 0, 0, 932, 169, 1, 0, 0, 0, 933, 934, 5, 37, 0, 0, 934, 171, 1, 0, 0, 0, 935, 936, 3, 46, 15, 0, 936, 937, 1, 0, 0, 0, 937, 938, 6, 78, 13, 0, 938, 173, 1, 0, 0, 0, 939, 942, 3, 140, 62, 0, 940, 943, 3, 76, 30, 0, 941, 943, 3, 90, 37, 0, 942, 940, 1, 0, 0, 0, 942, 941, 1, 0, 0, 0, 943, 947, 1, 0, 0, 0, 944, 946, 3, 92, 38, 0, 945, 944, 1, 0, 0, 0, 946, 949, 1, 0, 0, 0, 947, 945, 1, 0, 0, 0, 947, 948, 1, 0, 0, 0, 948, 957, 1, 0, 0, 0, 949, 947, 1, 0, 0, 0, 950, 952, 3, 140, 62, 0, 951, 953, 3, 74, 29, 0, 952, 951, 1, 0, 0, 0, 953, 954, 1, 0, 0, 0, 954, 952, 1, 0, 0, 0, 954, 955, 1, 0, 0, 0, 955, 957, 1, 0, 0, 0, 956, 939, 1, 0, 0, 0, 956, 950, 1, 0, 0, 0, 957, 175, 1, 0, 0, 0, 958, 959, 5, 91, 0, 0, 959, 960, 1, 0, 0, 0, 960, 961, 6, 80, 0, 0, 961, 962, 6, 80, 0, 0, 962, 177, 1, 0, 0, 0, 963, 964, 5, 93, 0, 0, 964, 965, 1, 0, 0, 0, 965, 966, 6, 81, 12, 0, 966, 967, 6, 81, 12, 0, 967, 179, 1, 0, 0, 0, 968, 972, 3, 76, 30, 0, 969, 971, 3, 92, 38, 0, 970, 969, 1, 0, 0, 0, 971, 974, 1, 0, 0, 0, 972, 970, 1, 0, 0, 0, 972, 973, 1, 0, 0, 0, 973, 985, 1, 0, 0, 0, 974, 972, 1, 0, 0, 0, 975, 978, 3, 90, 37, 0, 976, 978, 3, 84, 34, 0, 977, 975, 1, 0, 0, 0, 977, 976, 1, 0, 0, 0, 978, 980, 1, 0, 0, 0, 979, 981, 3, 92, 38, 0, 980, 979, 1, 0, 0, 0, 981, 982, 1, 0, 0, 0, 982, 980, 1, 0, 0, 0, 982, 983, 1, 0, 0, 0, 983, 985, 1, 0, 0, 0, 984, 968, 1, 0, 0, 0, 984, 977, 1, 0, 0, 0, 985, 181, 1, 0, 0, 0, 986, 988, 3, 86, 35, 0, 987, 989, 3, 88, 36, 0, 988, 987, 1, 0, 0, 0, 989, 990, 1, 0, 0, 0, 990, 988, 1, 0, 0, 0, 990, 991, 1, 0, 0, 0, 991, 992, 1, 0, 0, 0, 992, 993, 3, 86, 35, 0, 993, 183, 1, 0, 0, 0, 994, 995, 3, 182, 83, 0, 995, 185, 1, 0, 0, 0, 996, 997, 3, 66, 25, 0, 997, 998, 1, 0, 0, 0, 998, 999, 6, 85, 11, 0, 999, 187, 1, 0, 0, 0, 1000, 1001, 3, 68, 26, 0, 1001, 1002, 1, 0, 0, 0, 1002, 1003, 6, 86, 11, 0, 1003, 189, 1, 0, 0, 0, 1004, 1005, 3, 70, 27, 0, 1005, 1006, 1, 0, 0, 0, 1006, 1007, 6, 87, 11, 0, 1007, 191, 1, 0, 0, 0, 1008, 1009, 3, 176, 80, 0, 1009, 1010, 1, 0, 0, 0, 1010, 1011, 6, 88, 14, 0, 1011, 1012, 6, 88, 15, 0, 1012, 193, 1, 0, 0, 0, 1013, 1014, 3, 72, 28, 0, 1014, 1015, 1, 0, 0, 0, 1015, 1016, 6, 89, 16, 0, 1016, 1017, 6, 89, 12, 0, 1017, 195, 1, 0, 0, 0, 1018, 1019, 3, 70, 27, 0, 1019, 1020, 1, 0, 0, 0, 1020, 1021, 6, 90, 11, 0, 1021, 197, 1, 0, 0, 0, 1022, 1023, 3, 66, 25, 0, 1023, 1024, 1, 0, 0, 0, 1024, 1025, 6, 91, 11, 0, 1025, 199, 1, 0, 0, 0, 1026, 1027, 3, 68, 26, 0, 1027, 1028, 1, 0, 0, 0, 1028, 1029, 6, 92, 11, 0, 1029, 201, 1, 0, 0, 0, 1030, 1031, 3, 72, 28, 0, 1031, 1032, 1, 0, 0, 0, 1032, 1033, 6, 93, 16, 0, 1033, 1034, 6, 93, 12, 0, 1034, 203, 1, 0, 0, 0, 1035, 1036, 3, 176, 80, 0, 1036, 1037, 1, 0, 0, 0, 1037, 1038, 6, 94, 14, 0, 1038, 205, 1, 0, 0, 0, 1039, 1040, 3, 178, 81, 0, 1040, 1041, 1, 0, 0, 0, 1041, 1042, 6, 95, 17, 0, 1042, 207, 1, 0, 0, 0, 1043, 1044, 3, 110, 47, 0, 1044, 1045, 1, 0, 0, 0, 1045, 1046, 6, 96, 18, 0, 1046, 209, 1, 0, 0, 0, 1047, 1048, 3, 112, 48, 0, 1048, 1049, 1, 0, 0, 0, 1049, 1050, 6, 97, 19, 0, 1050, 211, 1, 0, 0, 0, 1051, 1052, 3, 106, 45, 0, 1052, 1053, 1, 0, 0, 0, 1053, 1054, 6, 98, 20, 0, 1054, 213, 1, 0, 0, 0, 1055, 1056, 7, 16, 0, 0, 1056, 1057, 7, 3, 0, 0, 1057, 1058, 7, 5, 0, 0, 1058, 1059, 7, 12, 0, 0, 1059, 1060, 7, 0, 0, 0, 1060, 1061, 7, 12, 0, 0, 1061, 1062, 7, 5, 0, 0, 1062, 1063, 7, 12, 0, 0, 1063, 215, 1, 0, 0, 0, 1064, 1068, 8, 33, 0, 0, 1065, 1066, 5, 47, 0, 0, 1066, 1068, 8, 34, 0, 0, 1067, 1064, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 217, 1, 0, 0, 0, 1069, 1071, 3, 216, 100, 0, 1070, 1069, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1070, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 219, 1, 0, 0, 0, 1074, 1075, 3, 218, 101, 0, 1075, 1076, 1, 0, 0, 0, 1076, 1077, 6, 102, 21, 0, 1077, 221, 1, 0, 0, 0, 1078, 1079, 3, 94, 39, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 6, 103, 22, 0, 1081, 223, 1, 0, 0, 0, 1082, 1083, 3, 66, 25, 0, 1083, 1084, 1, 0, 0, 0, 1084, 1085, 6, 104, 11, 0, 1085, 225, 1, 0, 0, 0, 1086, 1087, 3, 68, 26, 0, 1087, 1088, 1, 0, 0, 0, 1088, 1089, 6, 105, 11, 0, 1089, 227, 1, 0, 0, 0, 1090, 1091, 3, 70, 27, 0, 1091, 1092, 1, 0, 0, 0, 1092, 1093, 6, 106, 11, 0, 1093, 229, 1, 0, 0, 0, 1094, 1095, 3, 72, 28, 0, 1095, 1096, 1, 0, 0, 0, 1096, 1097, 6, 107, 16, 0, 1097, 1098, 6, 107, 12, 0, 1098, 231, 1, 0, 0, 0, 1099, 1100, 3, 116, 50, 0, 1100, 1101, 1, 0, 0, 0, 1101, 1102, 6, 108, 23, 0, 1102, 233, 1, 0, 0, 0, 1103, 1104, 3, 112, 48, 0, 1104, 1105, 1, 0, 0, 0, 1105, 1106, 6, 109, 19, 0, 1106, 235, 1, 0, 0, 0, 1107, 1108, 4, 110, 8, 0, 1108, 1109, 3, 140, 62, 0, 1109, 1110, 1, 0, 0, 0, 1110, 1111, 6, 110, 24, 0, 1111, 237, 1, 0, 0, 0, 1112, 1113, 4, 111, 9, 0, 1113, 1114, 3, 174, 79, 0, 1114, 1115, 1, 0, 0, 0, 1115, 1116, 6, 111, 25, 0, 1116, 239, 1, 0, 0, 0, 1117, 1122, 3, 76, 30, 0, 1118, 1122, 3, 74, 29, 0, 1119, 1122, 3, 90, 37, 0, 1120, 1122, 3, 166, 75, 0, 1121, 1117, 1, 0, 0, 0, 1121, 1118, 1, 0, 0, 0, 1121, 1119, 1, 0, 0, 0, 1121, 1120, 1, 0, 0, 0, 1122, 241, 1, 0, 0, 0, 1123, 1126, 3, 76, 30, 0, 1124, 1126, 3, 166, 75, 0, 1125, 1123, 1, 0, 0, 0, 1125, 1124, 1, 0, 0, 0, 1126, 1130, 1, 0, 0, 0, 1127, 1129, 3, 240, 112, 0, 1128, 1127, 1, 0, 0, 0, 1129, 1132, 1, 0, 0, 0, 1130, 1128, 1, 0, 0, 0, 1130, 1131, 1, 0, 0, 0, 1131, 1143, 1, 0, 0, 0, 1132, 1130, 1, 0, 0, 0, 1133, 1136, 3, 90, 37, 0, 1134, 1136, 3, 84, 34, 0, 1135, 1133, 1, 0, 0, 0, 1135, 1134, 1, 0, 0, 0, 1136, 1138, 1, 0, 0, 0, 1137, 1139, 3, 240, 112, 0, 1138, 1137, 1, 0, 0, 0, 1139, 1140, 1, 0, 0, 0, 1140, 1138, 1, 0, 0, 0, 1140, 1141, 1, 0, 0, 0, 1141, 1143, 1, 0, 0, 0, 1142, 1125, 1, 0, 0, 0, 1142, 1135, 1, 0, 0, 0, 1143, 243, 1, 0, 0, 0, 1144, 1147, 3, 242, 113, 0, 1145, 1147, 3, 182, 83, 0, 1146, 1144, 1, 0, 0, 0, 1146, 1145, 1, 0, 0, 0, 1147, 1148, 1, 0, 0, 0, 1148, 1146, 1, 0, 0, 0, 1148, 1149, 1, 0, 0, 0, 1149, 245, 1, 0, 0, 0, 1150, 1151, 3, 66, 25, 0, 1151, 1152, 1, 0, 0, 0, 1152, 1153, 6, 115, 11, 0, 1153, 247, 1, 0, 0, 0, 1154, 1155, 3, 68, 26, 0, 1155, 1156, 1, 0, 0, 0, 1156, 1157, 6, 116, 11, 0, 1157, 249, 1, 0, 0, 0, 1158, 1159, 3, 70, 27, 0, 1159, 1160, 1, 0, 0, 0, 1160, 1161, 6, 117, 11, 0, 1161, 251, 1, 0, 0, 0, 1162, 1163, 3, 72, 28, 0, 1163, 1164, 1, 0, 0, 0, 1164, 1165, 6, 118, 16, 0, 1165, 1166, 6, 118, 12, 0, 1166, 253, 1, 0, 0, 0, 1167, 1168, 3, 106, 45, 0, 1168, 1169, 1, 0, 0, 0, 1169, 1170, 6, 119, 20, 0, 1170, 255, 1, 0, 0, 0, 1171, 1172, 3, 112, 48, 0, 1172, 1173, 1, 0, 0, 0, 1173, 1174, 6, 120, 19, 0, 1174, 257, 1, 0, 0, 0, 1175, 1176, 3, 116, 50, 0, 1176, 1177, 1, 0, 0, 0, 1177, 1178, 6, 121, 23, 0, 1178, 259, 1, 0, 0, 0, 1179, 1180, 4, 122, 10, 0, 1180, 1181, 3, 140, 62, 0, 1181, 1182, 1, 0, 0, 0, 1182, 1183, 6, 122, 24, 0, 1183, 261, 1, 0, 0, 0, 1184, 1185, 4, 123, 11, 0, 1185, 1186, 3, 174, 79, 0, 1186, 1187, 1, 0, 0, 0, 1187, 1188, 6, 123, 25, 0, 1188, 263, 1, 0, 0, 0, 1189, 1190, 7, 12, 0, 0, 1190, 1191, 7, 2, 0, 0, 1191, 265, 1, 0, 0, 0, 1192, 1193, 3, 244, 114, 0, 1193, 1194, 1, 0, 0, 0, 1194, 1195, 6, 125, 26, 0, 1195, 267, 1, 0, 0, 0, 1196, 1197, 3, 66, 25, 0, 1197, 1198, 1, 0, 0, 0, 1198, 1199, 6, 126, 11, 0, 1199, 269, 1, 0, 0, 0, 1200, 1201, 3, 68, 26, 0, 1201, 1202, 1, 0, 0, 0, 1202, 1203, 6, 127, 11, 0, 1203, 271, 1, 0, 0, 0, 1204, 1205, 3, 70, 27, 0, 1205, 1206, 1, 0, 0, 0, 1206, 1207, 6, 128, 11, 0, 1207, 273, 1, 0, 0, 0, 1208, 1209, 3, 72, 28, 0, 1209, 1210, 1, 0, 0, 0, 1210, 1211, 6, 129, 16, 0, 1211, 1212, 6, 129, 12, 0, 1212, 275, 1, 0, 0, 0, 1213, 1214, 3, 176, 80, 0, 1214, 1215, 1, 0, 0, 0, 1215, 1216, 6, 130, 14, 0, 1216, 1217, 6, 130, 27, 0, 1217, 277, 1, 0, 0, 0, 1218, 1219, 7, 7, 0, 0, 1219, 1220, 7, 9, 0, 0, 1220, 1221, 1, 0, 0, 0, 1221, 1222, 6, 131, 28, 0, 1222, 279, 1, 0, 0, 0, 1223, 1224, 7, 19, 0, 0, 1224, 1225, 7, 1, 0, 0, 1225, 1226, 7, 5, 0, 0, 1226, 1227, 7, 10, 0, 0, 1227, 1228, 1, 0, 0, 0, 1228, 1229, 6, 132, 28, 0, 1229, 281, 1, 0, 0, 0, 1230, 1231, 8, 35, 0, 0, 1231, 283, 1, 0, 0, 0, 1232, 1234, 3, 282, 133, 0, 1233, 1232, 1, 0, 0, 0, 1234, 1235, 1, 0, 0, 0, 1235, 1233, 1, 0, 0, 0, 1235, 1236, 1, 0, 0, 0, 1236, 1237, 1, 0, 0, 0, 1237, 1238, 3, 110, 47, 0, 1238, 1240, 1, 0, 0, 0, 1239, 1233, 1, 0, 0, 0, 1239, 1240, 1, 0, 0, 0, 1240, 1242, 1, 0, 0, 0, 1241, 1243, 3, 282, 133, 0, 1242, 1241, 1, 0, 0, 0, 1243, 1244, 1, 0, 0, 0, 1244, 1242, 1, 0, 0, 0, 1244, 1245, 1, 0, 0, 0, 1245, 285, 1, 0, 0, 0, 1246, 1247, 3, 284, 134, 0, 1247, 1248, 1, 0, 0, 0, 1248, 1249, 6, 135, 29, 0, 1249, 287, 1, 0, 0, 0, 1250, 1251, 3, 66, 25, 0, 1251, 1252, 1, 0, 0, 0, 1252, 1253, 6, 136, 11, 0, 1253, 289, 1, 0, 0, 0, 1254, 1255, 3, 68, 26, 0, 1255, 1256, 1, 0, 0, 0, 1256, 1257, 6, 137, 11, 0, 1257, 291, 1, 0, 0, 0, 1258, 1259, 3, 70, 27, 0, 1259, 1260, 1, 0, 0, 0, 1260, 1261, 6, 138, 11, 0, 1261, 293, 1, 0, 0, 0, 1262, 1263, 3, 72, 28, 0, 1263, 1264, 1, 0, 0, 0, 1264, 1265, 6, 139, 16, 0, 1265, 1266, 6, 139, 12, 0, 1266, 1267, 6, 139, 12, 0, 1267, 295, 1, 0, 0, 0, 1268, 1269, 3, 106, 45, 0, 1269, 1270, 1, 0, 0, 0, 1270, 1271, 6, 140, 20, 0, 1271, 297, 1, 0, 0, 0, 1272, 1273, 3, 112, 48, 0, 1273, 1274, 1, 0, 0, 0, 1274, 1275, 6, 141, 19, 0, 1275, 299, 1, 0, 0, 0, 1276, 1277, 3, 116, 50, 0, 1277, 1278, 1, 0, 0, 0, 1278, 1279, 6, 142, 23, 0, 1279, 301, 1, 0, 0, 0, 1280, 1281, 3, 280, 132, 0, 1281, 1282, 1, 0, 0, 0, 1282, 1283, 6, 143, 30, 0, 1283, 303, 1, 0, 0, 0, 1284, 1285, 3, 244, 114, 0, 1285, 1286, 1, 0, 0, 0, 1286, 1287, 6, 144, 26, 0, 1287, 305, 1, 0, 0, 0, 1288, 1289, 3, 184, 84, 0, 1289, 1290, 1, 0, 0, 0, 1290, 1291, 6, 145, 31, 0, 1291, 307, 1, 0, 0, 0, 1292, 1293, 4, 146, 12, 0, 1293, 1294, 3, 140, 62, 0, 1294, 1295, 1, 0, 0, 0, 1295, 1296, 6, 146, 24, 0, 1296, 309, 1, 0, 0, 0, 1297, 1298, 4, 147, 13, 0, 1298, 1299, 3, 174, 79, 0, 1299, 1300, 1, 0, 0, 0, 1300, 1301, 6, 147, 25, 0, 1301, 311, 1, 0, 0, 0, 1302, 1303, 3, 66, 25, 0, 1303, 1304, 1, 0, 0, 0, 1304, 1305, 6, 148, 11, 0, 1305, 313, 1, 0, 0, 0, 1306, 1307, 3, 68, 26, 0, 1307, 1308, 1, 0, 0, 0, 1308, 1309, 6, 149, 11, 0, 1309, 315, 1, 0, 0, 0, 1310, 1311, 3, 70, 27, 0, 1311, 1312, 1, 0, 0, 0, 1312, 1313, 6, 150, 11, 0, 1313, 317, 1, 0, 0, 0, 1314, 1315, 3, 72, 28, 0, 1315, 1316, 1, 0, 0, 0, 1316, 1317, 6, 151, 16, 0, 1317, 1318, 6, 151, 12, 0, 1318, 319, 1, 0, 0, 0, 1319, 1320, 3, 116, 50, 0, 1320, 1321, 1, 0, 0, 0, 1321, 1322, 6, 152, 23, 0, 1322, 321, 1, 0, 0, 0, 1323, 1324, 4, 153, 14, 0, 1324, 1325, 3, 140, 62, 0, 1325, 1326, 1, 0, 0, 0, 1326, 1327, 6, 153, 24, 0, 1327, 323, 1, 0, 0, 0, 1328, 1329, 4, 154, 15, 0, 1329, 1330, 3, 174, 79, 0, 1330, 1331, 1, 0, 0, 0, 1331, 1332, 6, 154, 25, 0, 1332, 325, 1, 0, 0, 0, 1333, 1334, 3, 184, 84, 0, 1334, 1335, 1, 0, 0, 0, 1335, 1336, 6, 155, 31, 0, 1336, 327, 1, 0, 0, 0, 1337, 1338, 3, 180, 82, 0, 1338, 1339, 1, 0, 0, 0, 1339, 1340, 6, 156, 32, 0, 1340, 329, 1, 0, 0, 0, 1341, 1342, 3, 66, 25, 0, 1342, 1343, 1, 0, 0, 0, 1343, 1344, 6, 157, 11, 0, 1344, 331, 1, 0, 0, 0, 1345, 1346, 3, 68, 26, 0, 1346, 1347, 1, 0, 0, 0, 1347, 1348, 6, 158, 11, 0, 1348, 333, 1, 0, 0, 0, 1349, 1350, 3, 70, 27, 0, 1350, 1351, 1, 0, 0, 0, 1351, 1352, 6, 159, 11, 0, 1352, 335, 1, 0, 0, 0, 1353, 1354, 3, 72, 28, 0, 1354, 1355, 1, 0, 0, 0, 1355, 1356, 6, 160, 16, 0, 1356, 1357, 6, 160, 12, 0, 1357, 337, 1, 0, 0, 0, 1358, 1359, 7, 1, 0, 0, 1359, 1360, 7, 9, 0, 0, 1360, 1361, 7, 15, 0, 0, 1361, 1362, 7, 7, 0, 0, 1362, 339, 1, 0, 0, 0, 1363, 1364, 3, 66, 25, 0, 1364, 1365, 1, 0, 0, 0, 1365, 1366, 6, 162, 11, 0, 1366, 341, 1, 0, 0, 0, 1367, 1368, 3, 68, 26, 0, 1368, 1369, 1, 0, 0, 0, 1369, 1370, 6, 163, 11, 0, 1370, 343, 1, 0, 0, 0, 1371, 1372, 3, 70, 27, 0, 1372, 1373, 1, 0, 0, 0, 1373, 1374, 6, 164, 11, 0, 1374, 345, 1, 0, 0, 0, 1375, 1376, 3, 178, 81, 0, 1376, 1377, 1, 0, 0, 0, 1377, 1378, 6, 165, 17, 0, 1378, 1379, 6, 165, 12, 0, 1379, 347, 1, 0, 0, 0, 1380, 1381, 3, 110, 47, 0, 1381, 1382, 1, 0, 0, 0, 1382, 1383, 6, 166, 18, 0, 1383, 349, 1, 0, 0, 0, 1384, 1390, 3, 84, 34, 0, 1385, 1390, 3, 74, 29, 0, 1386, 1390, 3, 116, 50, 0, 1387, 1390, 3, 76, 30, 0, 1388, 1390, 3, 90, 37, 0, 1389, 1384, 1, 0, 0, 0, 1389, 1385, 1, 0, 0, 0, 1389, 1386, 1, 0, 0, 0, 1389, 1387, 1, 0, 0, 0, 1389, 1388, 1, 0, 0, 0, 1390, 1391, 1, 0, 0, 0, 1391, 1389, 1, 0, 0, 0, 1391, 1392, 1, 0, 0, 0, 1392, 351, 1, 0, 0, 0, 1393, 1394, 3, 66, 25, 0, 1394, 1395, 1, 0, 0, 0, 1395, 1396, 6, 168, 11, 0, 1396, 353, 1, 0, 0, 0, 1397, 1398, 3, 68, 26, 0, 1398, 1399, 1, 0, 0, 0, 1399, 1400, 6, 169, 11, 0, 1400, 355, 1, 0, 0, 0, 1401, 1402, 3, 70, 27, 0, 1402, 1403, 1, 0, 0, 0, 1403, 1404, 6, 170, 11, 0, 1404, 357, 1, 0, 0, 0, 1405, 1406, 3, 72, 28, 0, 1406, 1407, 1, 0, 0, 0, 1407, 1408, 6, 171, 16, 0, 1408, 1409, 6, 171, 12, 0, 1409, 359, 1, 0, 0, 0, 1410, 1411, 3, 110, 47, 0, 1411, 1412, 1, 0, 0, 0, 1412, 1413, 6, 172, 18, 0, 1413, 361, 1, 0, 0, 0, 1414, 1415, 3, 112, 48, 0, 1415, 1416, 1, 0, 0, 0, 1416, 1417, 6, 173, 19, 0, 1417, 363, 1, 0, 0, 0, 1418, 1419, 3, 116, 50, 0, 1419, 1420, 1, 0, 0, 0, 1420, 1421, 6, 174, 23, 0, 1421, 365, 1, 0, 0, 0, 1422, 1423, 3, 278, 131, 0, 1423, 1424, 1, 0, 0, 0, 1424, 1425, 6, 175, 33, 0, 1425, 1426, 6, 175, 34, 0, 1426, 367, 1, 0, 0, 0, 1427, 1428, 3, 218, 101, 0, 1428, 1429, 1, 0, 0, 0, 1429, 1430, 6, 176, 21, 0, 1430, 369, 1, 0, 0, 0, 1431, 1432, 3, 94, 39, 0, 1432, 1433, 1, 0, 0, 0, 1433, 1434, 6, 177, 22, 0, 1434, 371, 1, 0, 0, 0, 1435, 1436, 3, 66, 25, 0, 1436, 1437, 1, 0, 0, 0, 1437, 1438, 6, 178, 11, 0, 1438, 373, 1, 0, 0, 0, 1439, 1440, 3, 68, 26, 0, 1440, 1441, 1, 0, 0, 0, 1441, 1442, 6, 179, 11, 0, 1442, 375, 1, 0, 0, 0, 1443, 1444, 3, 70, 27, 0, 1444, 1445, 1, 0, 0, 0, 1445, 1446, 6, 180, 11, 0, 1446, 377, 1, 0, 0, 0, 1447, 1448, 3, 72, 28, 0, 1448, 1449, 1, 0, 0, 0, 1449, 1450, 6, 181, 16, 0, 1450, 1451, 6, 181, 12, 0, 1451, 1452, 6, 181, 12, 0, 1452, 379, 1, 0, 0, 0, 1453, 1454, 3, 112, 48, 0, 1454, 1455, 1, 0, 0, 0, 1455, 1456, 6, 182, 19, 0, 1456, 381, 1, 0, 0, 0, 1457, 1458, 3, 116, 50, 0, 1458, 1459, 1, 0, 0, 0, 1459, 1460, 6, 183, 23, 0, 1460, 383, 1, 0, 0, 0, 1461, 1462, 3, 244, 114, 0, 1462, 1463, 1, 0, 0, 0, 1463, 1464, 6, 184, 26, 0, 1464, 385, 1, 0, 0, 0, 1465, 1466, 3, 66, 25, 0, 1466, 1467, 1, 0, 0, 0, 1467, 1468, 6, 185, 11, 0, 1468, 387, 1, 0, 0, 0, 1469, 1470, 3, 68, 26, 0, 1470, 1471, 1, 0, 0, 0, 1471, 1472, 6, 186, 11, 0, 1472, 389, 1, 0, 0, 0, 1473, 1474, 3, 70, 27, 0, 1474, 1475, 1, 0, 0, 0, 1475, 1476, 6, 187, 11, 0, 1476, 391, 1, 0, 0, 0, 1477, 1478, 3, 72, 28, 0, 1478, 1479, 1, 0, 0, 0, 1479, 1480, 6, 188, 16, 0, 1480, 1481, 6, 188, 12, 0, 1481, 393, 1, 0, 0, 0, 1482, 1483, 3, 54, 19, 0, 1483, 1484, 1, 0, 0, 0, 1484, 1485, 6, 189, 35, 0, 1485, 395, 1, 0, 0, 0, 1486, 1487, 3, 264, 124, 0, 1487, 1488, 1, 0, 0, 0, 1488, 1489, 6, 190, 36, 0, 1489, 397, 1, 0, 0, 0, 1490, 1491, 3, 278, 131, 0, 1491, 1492, 1, 0, 0, 0, 1492, 1493, 6, 191, 33, 0, 1493, 1494, 6, 191, 12, 0, 1494, 1495, 6, 191, 0, 0, 1495, 399, 1, 0, 0, 0, 1496, 1497, 7, 20, 0, 0, 1497, 1498, 7, 2, 0, 0, 1498, 1499, 7, 1, 0, 0, 1499, 1500, 7, 9, 0, 0, 1500, 1501, 7, 17, 0, 0, 1501, 1502, 1, 0, 0, 0, 1502, 1503, 6, 192, 12, 0, 1503, 1504, 6, 192, 0, 0, 1504, 401, 1, 0, 0, 0, 1505, 1506, 3, 180, 82, 0, 1506, 1507, 1, 0, 0, 0, 1507, 1508, 6, 193, 32, 0, 1508, 403, 1, 0, 0, 0, 1509, 1510, 3, 184, 84, 0, 1510, 1511, 1, 0, 0, 0, 1511, 1512, 6, 194, 31, 0, 1512, 405, 1, 0, 0, 0, 1513, 1514, 3, 66, 25, 0, 1514, 1515, 1, 0, 0, 0, 1515, 1516, 6, 195, 11, 0, 1516, 407, 1, 0, 0, 0, 1517, 1518, 3, 68, 26, 0, 1518, 1519, 1, 0, 0, 0, 1519, 1520, 6, 196, 11, 0, 1520, 409, 1, 0, 0, 0, 1521, 1522, 3, 70, 27, 0, 1522, 1523, 1, 0, 0, 0, 1523, 1524, 6, 197, 11, 0, 1524, 411, 1, 0, 0, 0, 1525, 1526, 3, 72, 28, 0, 1526, 1527, 1, 0, 0, 0, 1527, 1528, 6, 198, 16, 0, 1528, 1529, 6, 198, 12, 0, 1529, 413, 1, 0, 0, 0, 1530, 1531, 3, 218, 101, 0, 1531, 1532, 1, 0, 0, 0, 1532, 1533, 6, 199, 21, 0, 1533, 1534, 6, 199, 12, 0, 1534, 1535, 6, 199, 37, 0, 1535, 415, 1, 0, 0, 0, 1536, 1537, 3, 94, 39, 0, 1537, 1538, 1, 0, 0, 0, 1538, 1539, 6, 200, 22, 0, 1539, 1540, 6, 200, 12, 0, 1540, 1541, 6, 200, 37, 0, 1541, 417, 1, 0, 0, 0, 1542, 1543, 3, 66, 25, 0, 1543, 1544, 1, 0, 0, 0, 1544, 1545, 6, 201, 11, 0, 1545, 419, 1, 0, 0, 0, 1546, 1547, 3, 68, 26, 0, 1547, 1548, 1, 0, 0, 0, 1548, 1549, 6, 202, 11, 0, 1549, 421, 1, 0, 0, 0, 1550, 1551, 3, 70, 27, 0, 1551, 1552, 1, 0, 0, 0, 1552, 1553, 6, 203, 11, 0, 1553, 423, 1, 0, 0, 0, 1554, 1555, 3, 110, 47, 0, 1555, 1556, 1, 0, 0, 0, 1556, 1557, 6, 204, 18, 0, 1557, 1558, 6, 204, 12, 0, 1558, 1559, 6, 204, 9, 0, 1559, 425, 1, 0, 0, 0, 1560, 1561, 3, 112, 48, 0, 1561, 1562, 1, 0, 0, 0, 1562, 1563, 6, 205, 19, 0, 1563, 1564, 6, 205, 12, 0, 1564, 1565, 6, 205, 9, 0, 1565, 427, 1, 0, 0, 0, 1566, 1567, 3, 66, 25, 0, 1567, 1568, 1, 0, 0, 0, 1568, 1569, 6, 206, 11, 0, 1569, 429, 1, 0, 0, 0, 1570, 1571, 3, 68, 26, 0, 1571, 1572, 1, 0, 0, 0, 1572, 1573, 6, 207, 11, 0, 1573, 431, 1, 0, 0, 0, 1574, 1575, 3, 70, 27, 0, 1575, 1576, 1, 0, 0, 0, 1576, 1577, 6, 208, 11, 0, 1577, 433, 1, 0, 0, 0, 1578, 1579, 3, 184, 84, 0, 1579, 1580, 1, 0, 0, 0, 1580, 1581, 6, 209, 12, 0, 1581, 1582, 6, 209, 0, 0, 1582, 1583, 6, 209, 31, 0, 1583, 435, 1, 0, 0, 0, 1584, 1585, 3, 180, 82, 0, 1585, 1586, 1, 0, 0, 0, 1586, 1587, 6, 210, 12, 0, 1587, 1588, 6, 210, 0, 0, 1588, 1589, 6, 210, 32, 0, 1589, 437, 1, 0, 0, 0, 1590, 1591, 3, 100, 42, 0, 1591, 1592, 1, 0, 0, 0, 1592, 1593, 6, 211, 12, 0, 1593, 1594, 6, 211, 0, 0, 1594, 1595, 6, 211, 38, 0, 1595, 439, 1, 0, 0, 0, 1596, 1597, 3, 72, 28, 0, 1597, 1598, 1, 0, 0, 0, 1598, 1599, 6, 212, 16, 0, 1599, 1600, 6, 212, 12, 0, 1600, 441, 1, 0, 0, 0, 66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 655, 665, 669, 672, 681, 683, 694, 713, 718, 727, 734, 739, 741, 752, 760, 763, 765, 770, 775, 781, 788, 793, 799, 802, 810, 814, 942, 947, 954, 956, 972, 977, 982, 984, 990, 1067, 1072, 1121, 1125, 1130, 1135, 1140, 1142, 1146, 1148, 1235, 1239, 1244, 1389, 1391, 39, 5, 1, 0, 5, 4, 0, 5, 6, 0, 5, 2, 0, 5, 3, 0, 5, 8, 0, 5, 5, 0, 5, 9, 0, 5, 11, 0, 5, 14, 0, 5, 13, 0, 0, 1, 0, 4, 0, 0, 7, 16, 0, 7, 70, 0, 5, 0, 0, 7, 29, 0, 7, 71, 0, 7, 38, 0, 7, 39, 0, 7, 36, 0, 7, 81, 0, 7, 30, 0, 7, 41, 0, 7, 53, 0, 7, 69, 0, 7, 85, 0, 5, 10, 0, 5, 7, 0, 7, 95, 0, 7, 94, 0, 7, 73, 0, 7, 72, 0, 7, 93, 0, 5, 12, 0, 7, 20, 0, 7, 89, 0, 5, 15, 0, 7, 33, 0] \ No newline at end of file diff --git a/packages/kbn-esql-ast/src/antlr/esql_lexer.tokens b/packages/kbn-esql-ast/src/antlr/esql_lexer.tokens index 3dd1a2c754038..b1a16987dd8ce 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_lexer.tokens +++ b/packages/kbn-esql-ast/src/antlr/esql_lexer.tokens @@ -17,106 +17,115 @@ WHERE=16 DEV_INLINESTATS=17 DEV_LOOKUP=18 DEV_METRICS=19 -UNKNOWN_CMD=20 -LINE_COMMENT=21 -MULTILINE_COMMENT=22 -WS=23 -COLON=24 -PIPE=25 -QUOTED_STRING=26 -INTEGER_LITERAL=27 -DECIMAL_LITERAL=28 -BY=29 -AND=30 -ASC=31 -ASSIGN=32 -CAST_OP=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -IN=39 -IS=40 -LAST=41 -LIKE=42 -LP=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -EQ=52 -CIEQ=53 -NEQ=54 -LT=55 -LTE=56 -GT=57 -GTE=58 -PLUS=59 -MINUS=60 -ASTERISK=61 -SLASH=62 -PERCENT=63 -NAMED_OR_POSITIONAL_PARAM=64 -OPENING_BRACKET=65 -CLOSING_BRACKET=66 -UNQUOTED_IDENTIFIER=67 -QUOTED_IDENTIFIER=68 -EXPR_LINE_COMMENT=69 -EXPR_MULTILINE_COMMENT=70 -EXPR_WS=71 -EXPLAIN_WS=72 -EXPLAIN_LINE_COMMENT=73 -EXPLAIN_MULTILINE_COMMENT=74 -METADATA=75 -UNQUOTED_SOURCE=76 -FROM_LINE_COMMENT=77 -FROM_MULTILINE_COMMENT=78 -FROM_WS=79 -ID_PATTERN=80 -PROJECT_LINE_COMMENT=81 -PROJECT_MULTILINE_COMMENT=82 -PROJECT_WS=83 -AS=84 -RENAME_LINE_COMMENT=85 -RENAME_MULTILINE_COMMENT=86 -RENAME_WS=87 -ON=88 -WITH=89 -ENRICH_POLICY_NAME=90 -ENRICH_LINE_COMMENT=91 -ENRICH_MULTILINE_COMMENT=92 -ENRICH_WS=93 -ENRICH_FIELD_LINE_COMMENT=94 -ENRICH_FIELD_MULTILINE_COMMENT=95 -ENRICH_FIELD_WS=96 -MVEXPAND_LINE_COMMENT=97 -MVEXPAND_MULTILINE_COMMENT=98 -MVEXPAND_WS=99 -INFO=100 -SHOW_LINE_COMMENT=101 -SHOW_MULTILINE_COMMENT=102 -SHOW_WS=103 -SETTING=104 -SETTING_LINE_COMMENT=105 -SETTTING_MULTILINE_COMMENT=106 -SETTING_WS=107 -LOOKUP_LINE_COMMENT=108 -LOOKUP_MULTILINE_COMMENT=109 -LOOKUP_WS=110 -LOOKUP_FIELD_LINE_COMMENT=111 -LOOKUP_FIELD_MULTILINE_COMMENT=112 -LOOKUP_FIELD_WS=113 -METRICS_LINE_COMMENT=114 -METRICS_MULTILINE_COMMENT=115 -METRICS_WS=116 -CLOSING_METRICS_LINE_COMMENT=117 -CLOSING_METRICS_MULTILINE_COMMENT=118 -CLOSING_METRICS_WS=119 +DEV_JOIN=20 +DEV_JOIN_FULL=21 +DEV_JOIN_LEFT=22 +DEV_JOIN_RIGHT=23 +DEV_JOIN_LOOKUP=24 +UNKNOWN_CMD=25 +LINE_COMMENT=26 +MULTILINE_COMMENT=27 +WS=28 +PIPE=29 +QUOTED_STRING=30 +INTEGER_LITERAL=31 +DECIMAL_LITERAL=32 +BY=33 +AND=34 +ASC=35 +ASSIGN=36 +CAST_OP=37 +COLON=38 +COMMA=39 +DESC=40 +DOT=41 +FALSE=42 +FIRST=43 +IN=44 +IS=45 +LAST=46 +LIKE=47 +LP=48 +NOT=49 +NULL=50 +NULLS=51 +OR=52 +PARAM=53 +RLIKE=54 +RP=55 +TRUE=56 +EQ=57 +CIEQ=58 +NEQ=59 +LT=60 +LTE=61 +GT=62 +GTE=63 +PLUS=64 +MINUS=65 +ASTERISK=66 +SLASH=67 +PERCENT=68 +NAMED_OR_POSITIONAL_PARAM=69 +OPENING_BRACKET=70 +CLOSING_BRACKET=71 +UNQUOTED_IDENTIFIER=72 +QUOTED_IDENTIFIER=73 +EXPR_LINE_COMMENT=74 +EXPR_MULTILINE_COMMENT=75 +EXPR_WS=76 +EXPLAIN_WS=77 +EXPLAIN_LINE_COMMENT=78 +EXPLAIN_MULTILINE_COMMENT=79 +METADATA=80 +UNQUOTED_SOURCE=81 +FROM_LINE_COMMENT=82 +FROM_MULTILINE_COMMENT=83 +FROM_WS=84 +ID_PATTERN=85 +PROJECT_LINE_COMMENT=86 +PROJECT_MULTILINE_COMMENT=87 +PROJECT_WS=88 +AS=89 +RENAME_LINE_COMMENT=90 +RENAME_MULTILINE_COMMENT=91 +RENAME_WS=92 +ON=93 +WITH=94 +ENRICH_POLICY_NAME=95 +ENRICH_LINE_COMMENT=96 +ENRICH_MULTILINE_COMMENT=97 +ENRICH_WS=98 +ENRICH_FIELD_LINE_COMMENT=99 +ENRICH_FIELD_MULTILINE_COMMENT=100 +ENRICH_FIELD_WS=101 +MVEXPAND_LINE_COMMENT=102 +MVEXPAND_MULTILINE_COMMENT=103 +MVEXPAND_WS=104 +INFO=105 +SHOW_LINE_COMMENT=106 +SHOW_MULTILINE_COMMENT=107 +SHOW_WS=108 +SETTING=109 +SETTING_LINE_COMMENT=110 +SETTTING_MULTILINE_COMMENT=111 +SETTING_WS=112 +LOOKUP_LINE_COMMENT=113 +LOOKUP_MULTILINE_COMMENT=114 +LOOKUP_WS=115 +LOOKUP_FIELD_LINE_COMMENT=116 +LOOKUP_FIELD_MULTILINE_COMMENT=117 +LOOKUP_FIELD_WS=118 +USING=119 +JOIN_LINE_COMMENT=120 +JOIN_MULTILINE_COMMENT=121 +JOIN_WS=122 +METRICS_LINE_COMMENT=123 +METRICS_MULTILINE_COMMENT=124 +METRICS_WS=125 +CLOSING_METRICS_LINE_COMMENT=126 +CLOSING_METRICS_MULTILINE_COMMENT=127 +CLOSING_METRICS_WS=128 'dissect'=1 'drop'=2 'enrich'=3 @@ -133,46 +142,47 @@ CLOSING_METRICS_WS=119 'sort'=14 'stats'=15 'where'=16 -':'=24 -'|'=25 -'by'=29 -'and'=30 -'asc'=31 -'='=32 -'::'=33 -','=34 -'desc'=35 -'.'=36 -'false'=37 -'first'=38 -'in'=39 -'is'=40 -'last'=41 -'like'=42 -'('=43 -'not'=44 -'null'=45 -'nulls'=46 -'or'=47 -'?'=48 -'rlike'=49 -')'=50 -'true'=51 -'=='=52 -'=~'=53 -'!='=54 -'<'=55 -'<='=56 -'>'=57 -'>='=58 -'+'=59 -'-'=60 -'*'=61 -'/'=62 -'%'=63 -']'=66 -'metadata'=75 -'as'=84 -'on'=88 -'with'=89 -'info'=100 +'|'=29 +'by'=33 +'and'=34 +'asc'=35 +'='=36 +'::'=37 +':'=38 +','=39 +'desc'=40 +'.'=41 +'false'=42 +'first'=43 +'in'=44 +'is'=45 +'last'=46 +'like'=47 +'('=48 +'not'=49 +'null'=50 +'nulls'=51 +'or'=52 +'?'=53 +'rlike'=54 +')'=55 +'true'=56 +'=='=57 +'=~'=58 +'!='=59 +'<'=60 +'<='=61 +'>'=62 +'>='=63 +'+'=64 +'-'=65 +'*'=66 +'/'=67 +'%'=68 +']'=71 +'metadata'=80 +'as'=89 +'on'=93 +'with'=94 +'info'=105 +'USING'=119 diff --git a/packages/kbn-esql-ast/src/antlr/esql_lexer.ts b/packages/kbn-esql-ast/src/antlr/esql_lexer.ts index 54546fef85904..5c25d0233576f 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_lexer.ts +++ b/packages/kbn-esql-ast/src/antlr/esql_lexer.ts @@ -42,106 +42,115 @@ export default class esql_lexer extends lexer_config { public static readonly DEV_INLINESTATS = 17; public static readonly DEV_LOOKUP = 18; public static readonly DEV_METRICS = 19; - public static readonly UNKNOWN_CMD = 20; - public static readonly LINE_COMMENT = 21; - public static readonly MULTILINE_COMMENT = 22; - public static readonly WS = 23; - public static readonly COLON = 24; - public static readonly PIPE = 25; - public static readonly QUOTED_STRING = 26; - public static readonly INTEGER_LITERAL = 27; - public static readonly DECIMAL_LITERAL = 28; - public static readonly BY = 29; - public static readonly AND = 30; - public static readonly ASC = 31; - public static readonly ASSIGN = 32; - public static readonly CAST_OP = 33; - public static readonly COMMA = 34; - public static readonly DESC = 35; - public static readonly DOT = 36; - public static readonly FALSE = 37; - public static readonly FIRST = 38; - public static readonly IN = 39; - public static readonly IS = 40; - public static readonly LAST = 41; - public static readonly LIKE = 42; - public static readonly LP = 43; - public static readonly NOT = 44; - public static readonly NULL = 45; - public static readonly NULLS = 46; - public static readonly OR = 47; - public static readonly PARAM = 48; - public static readonly RLIKE = 49; - public static readonly RP = 50; - public static readonly TRUE = 51; - public static readonly EQ = 52; - public static readonly CIEQ = 53; - public static readonly NEQ = 54; - public static readonly LT = 55; - public static readonly LTE = 56; - public static readonly GT = 57; - public static readonly GTE = 58; - public static readonly PLUS = 59; - public static readonly MINUS = 60; - public static readonly ASTERISK = 61; - public static readonly SLASH = 62; - public static readonly PERCENT = 63; - public static readonly NAMED_OR_POSITIONAL_PARAM = 64; - public static readonly OPENING_BRACKET = 65; - public static readonly CLOSING_BRACKET = 66; - public static readonly UNQUOTED_IDENTIFIER = 67; - public static readonly QUOTED_IDENTIFIER = 68; - public static readonly EXPR_LINE_COMMENT = 69; - public static readonly EXPR_MULTILINE_COMMENT = 70; - public static readonly EXPR_WS = 71; - public static readonly EXPLAIN_WS = 72; - public static readonly EXPLAIN_LINE_COMMENT = 73; - public static readonly EXPLAIN_MULTILINE_COMMENT = 74; - public static readonly METADATA = 75; - public static readonly UNQUOTED_SOURCE = 76; - public static readonly FROM_LINE_COMMENT = 77; - public static readonly FROM_MULTILINE_COMMENT = 78; - public static readonly FROM_WS = 79; - public static readonly ID_PATTERN = 80; - public static readonly PROJECT_LINE_COMMENT = 81; - public static readonly PROJECT_MULTILINE_COMMENT = 82; - public static readonly PROJECT_WS = 83; - public static readonly AS = 84; - public static readonly RENAME_LINE_COMMENT = 85; - public static readonly RENAME_MULTILINE_COMMENT = 86; - public static readonly RENAME_WS = 87; - public static readonly ON = 88; - public static readonly WITH = 89; - public static readonly ENRICH_POLICY_NAME = 90; - public static readonly ENRICH_LINE_COMMENT = 91; - public static readonly ENRICH_MULTILINE_COMMENT = 92; - public static readonly ENRICH_WS = 93; - public static readonly ENRICH_FIELD_LINE_COMMENT = 94; - public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 95; - public static readonly ENRICH_FIELD_WS = 96; - public static readonly MVEXPAND_LINE_COMMENT = 97; - public static readonly MVEXPAND_MULTILINE_COMMENT = 98; - public static readonly MVEXPAND_WS = 99; - public static readonly INFO = 100; - public static readonly SHOW_LINE_COMMENT = 101; - public static readonly SHOW_MULTILINE_COMMENT = 102; - public static readonly SHOW_WS = 103; - public static readonly SETTING = 104; - public static readonly SETTING_LINE_COMMENT = 105; - public static readonly SETTTING_MULTILINE_COMMENT = 106; - public static readonly SETTING_WS = 107; - public static readonly LOOKUP_LINE_COMMENT = 108; - public static readonly LOOKUP_MULTILINE_COMMENT = 109; - public static readonly LOOKUP_WS = 110; - public static readonly LOOKUP_FIELD_LINE_COMMENT = 111; - public static readonly LOOKUP_FIELD_MULTILINE_COMMENT = 112; - public static readonly LOOKUP_FIELD_WS = 113; - public static readonly METRICS_LINE_COMMENT = 114; - public static readonly METRICS_MULTILINE_COMMENT = 115; - public static readonly METRICS_WS = 116; - public static readonly CLOSING_METRICS_LINE_COMMENT = 117; - public static readonly CLOSING_METRICS_MULTILINE_COMMENT = 118; - public static readonly CLOSING_METRICS_WS = 119; + public static readonly DEV_JOIN = 20; + public static readonly DEV_JOIN_FULL = 21; + public static readonly DEV_JOIN_LEFT = 22; + public static readonly DEV_JOIN_RIGHT = 23; + public static readonly DEV_JOIN_LOOKUP = 24; + public static readonly UNKNOWN_CMD = 25; + public static readonly LINE_COMMENT = 26; + public static readonly MULTILINE_COMMENT = 27; + public static readonly WS = 28; + public static readonly PIPE = 29; + public static readonly QUOTED_STRING = 30; + public static readonly INTEGER_LITERAL = 31; + public static readonly DECIMAL_LITERAL = 32; + public static readonly BY = 33; + public static readonly AND = 34; + public static readonly ASC = 35; + public static readonly ASSIGN = 36; + public static readonly CAST_OP = 37; + public static readonly COLON = 38; + public static readonly COMMA = 39; + public static readonly DESC = 40; + public static readonly DOT = 41; + public static readonly FALSE = 42; + public static readonly FIRST = 43; + public static readonly IN = 44; + public static readonly IS = 45; + public static readonly LAST = 46; + public static readonly LIKE = 47; + public static readonly LP = 48; + public static readonly NOT = 49; + public static readonly NULL = 50; + public static readonly NULLS = 51; + public static readonly OR = 52; + public static readonly PARAM = 53; + public static readonly RLIKE = 54; + public static readonly RP = 55; + public static readonly TRUE = 56; + public static readonly EQ = 57; + public static readonly CIEQ = 58; + public static readonly NEQ = 59; + public static readonly LT = 60; + public static readonly LTE = 61; + public static readonly GT = 62; + public static readonly GTE = 63; + public static readonly PLUS = 64; + public static readonly MINUS = 65; + public static readonly ASTERISK = 66; + public static readonly SLASH = 67; + public static readonly PERCENT = 68; + public static readonly NAMED_OR_POSITIONAL_PARAM = 69; + public static readonly OPENING_BRACKET = 70; + public static readonly CLOSING_BRACKET = 71; + public static readonly UNQUOTED_IDENTIFIER = 72; + public static readonly QUOTED_IDENTIFIER = 73; + public static readonly EXPR_LINE_COMMENT = 74; + public static readonly EXPR_MULTILINE_COMMENT = 75; + public static readonly EXPR_WS = 76; + public static readonly EXPLAIN_WS = 77; + public static readonly EXPLAIN_LINE_COMMENT = 78; + public static readonly EXPLAIN_MULTILINE_COMMENT = 79; + public static readonly METADATA = 80; + public static readonly UNQUOTED_SOURCE = 81; + public static readonly FROM_LINE_COMMENT = 82; + public static readonly FROM_MULTILINE_COMMENT = 83; + public static readonly FROM_WS = 84; + public static readonly ID_PATTERN = 85; + public static readonly PROJECT_LINE_COMMENT = 86; + public static readonly PROJECT_MULTILINE_COMMENT = 87; + public static readonly PROJECT_WS = 88; + public static readonly AS = 89; + public static readonly RENAME_LINE_COMMENT = 90; + public static readonly RENAME_MULTILINE_COMMENT = 91; + public static readonly RENAME_WS = 92; + public static readonly ON = 93; + public static readonly WITH = 94; + public static readonly ENRICH_POLICY_NAME = 95; + public static readonly ENRICH_LINE_COMMENT = 96; + public static readonly ENRICH_MULTILINE_COMMENT = 97; + public static readonly ENRICH_WS = 98; + public static readonly ENRICH_FIELD_LINE_COMMENT = 99; + public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 100; + public static readonly ENRICH_FIELD_WS = 101; + public static readonly MVEXPAND_LINE_COMMENT = 102; + public static readonly MVEXPAND_MULTILINE_COMMENT = 103; + public static readonly MVEXPAND_WS = 104; + public static readonly INFO = 105; + public static readonly SHOW_LINE_COMMENT = 106; + public static readonly SHOW_MULTILINE_COMMENT = 107; + public static readonly SHOW_WS = 108; + public static readonly SETTING = 109; + public static readonly SETTING_LINE_COMMENT = 110; + public static readonly SETTTING_MULTILINE_COMMENT = 111; + public static readonly SETTING_WS = 112; + public static readonly LOOKUP_LINE_COMMENT = 113; + public static readonly LOOKUP_MULTILINE_COMMENT = 114; + public static readonly LOOKUP_WS = 115; + public static readonly LOOKUP_FIELD_LINE_COMMENT = 116; + public static readonly LOOKUP_FIELD_MULTILINE_COMMENT = 117; + public static readonly LOOKUP_FIELD_WS = 118; + public static readonly USING = 119; + public static readonly JOIN_LINE_COMMENT = 120; + public static readonly JOIN_MULTILINE_COMMENT = 121; + public static readonly JOIN_WS = 122; + public static readonly METRICS_LINE_COMMENT = 123; + public static readonly METRICS_MULTILINE_COMMENT = 124; + public static readonly METRICS_WS = 125; + public static readonly CLOSING_METRICS_LINE_COMMENT = 126; + public static readonly CLOSING_METRICS_MULTILINE_COMMENT = 127; + public static readonly CLOSING_METRICS_WS = 128; public static readonly EOF = Token.EOF; public static readonly EXPRESSION_MODE = 1; public static readonly EXPLAIN_MODE = 2; @@ -155,8 +164,9 @@ export default class esql_lexer extends lexer_config { public static readonly SETTING_MODE = 10; public static readonly LOOKUP_MODE = 11; public static readonly LOOKUP_FIELD_MODE = 12; - public static readonly METRICS_MODE = 13; - public static readonly CLOSING_METRICS_MODE = 14; + public static readonly JOIN_MODE = 13; + public static readonly METRICS_MODE = 14; + public static readonly CLOSING_METRICS_MODE = 15; public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ]; public static readonly literalNames: (string | null)[] = [ null, "'dissect'", @@ -172,32 +182,35 @@ export default class esql_lexer extends lexer_config { null, null, null, null, null, null, - "':'", "'|'", + null, null, + null, null, + null, "'|'", null, null, null, "'by'", "'and'", "'asc'", "'='", "'::'", - "','", "'desc'", - "'.'", "'false'", - "'first'", "'in'", - "'is'", "'last'", - "'like'", "'('", - "'not'", "'null'", - "'nulls'", "'or'", - "'?'", "'rlike'", - "')'", "'true'", - "'=='", "'=~'", - "'!='", "'<'", - "'<='", "'>'", - "'>='", "'+'", - "'-'", "'*'", - "'/'", "'%'", + "':'", "','", + "'desc'", "'.'", + "'false'", "'first'", + "'in'", "'is'", + "'last'", "'like'", + "'('", "'not'", + "'null'", "'nulls'", + "'or'", "'?'", + "'rlike'", "')'", + "'true'", "'=='", + "'=~'", "'!='", + "'<'", "'<='", + "'>'", "'>='", + "'+'", "'-'", + "'*'", "'/'", + "'%'", null, + null, "']'", null, null, - "']'", null, null, null, null, null, null, null, - null, "'metadata'", + "'metadata'", null, null, null, null, null, null, @@ -210,7 +223,14 @@ export default class esql_lexer extends lexer_config { null, null, null, null, null, null, - "'info'" ]; + "'info'", null, + null, null, + null, null, + null, null, + null, null, + null, null, + null, null, + "'USING'" ]; public static readonly symbolicNames: (string | null)[] = [ null, "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", @@ -223,30 +243,36 @@ export default class esql_lexer extends lexer_config { "DEV_INLINESTATS", "DEV_LOOKUP", "DEV_METRICS", + "DEV_JOIN", + "DEV_JOIN_FULL", + "DEV_JOIN_LEFT", + "DEV_JOIN_RIGHT", + "DEV_JOIN_LOOKUP", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", - "WS", "COLON", - "PIPE", "QUOTED_STRING", + "WS", "PIPE", + "QUOTED_STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "CAST_OP", - "COMMA", "DESC", - "DOT", "FALSE", - "FIRST", "IN", - "IS", "LAST", - "LIKE", "LP", - "NOT", "NULL", - "NULLS", "OR", - "PARAM", "RLIKE", - "RP", "TRUE", - "EQ", "CIEQ", - "NEQ", "LT", - "LTE", "GT", - "GTE", "PLUS", - "MINUS", "ASTERISK", + "COLON", "COMMA", + "DESC", "DOT", + "FALSE", "FIRST", + "IN", "IS", + "LAST", "LIKE", + "LP", "NOT", + "NULL", "NULLS", + "OR", "PARAM", + "RLIKE", "RP", + "TRUE", "EQ", + "CIEQ", "NEQ", + "LT", "LTE", + "GT", "GTE", + "PLUS", "MINUS", + "ASTERISK", "SLASH", "PERCENT", "NAMED_OR_POSITIONAL_PARAM", "OPENING_BRACKET", @@ -295,6 +321,9 @@ export default class esql_lexer extends lexer_config { "LOOKUP_FIELD_LINE_COMMENT", "LOOKUP_FIELD_MULTILINE_COMMENT", "LOOKUP_FIELD_WS", + "USING", "JOIN_LINE_COMMENT", + "JOIN_MULTILINE_COMMENT", + "JOIN_WS", "METRICS_LINE_COMMENT", "METRICS_MULTILINE_COMMENT", "METRICS_WS", @@ -307,20 +336,21 @@ export default class esql_lexer extends lexer_config { "ENRICH_MODE", "ENRICH_FIELD_MODE", "MVEXPAND_MODE", "SHOW_MODE", "SETTING_MODE", "LOOKUP_MODE", - "LOOKUP_FIELD_MODE", "METRICS_MODE", - "CLOSING_METRICS_MODE", ]; + "LOOKUP_FIELD_MODE", "JOIN_MODE", + "METRICS_MODE", "CLOSING_METRICS_MODE", ]; public static readonly ruleNames: string[] = [ "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", "FROM", "GROK", "KEEP", "LIMIT", "MV_EXPAND", "RENAME", "ROW", "SHOW", "SORT", "STATS", "WHERE", - "DEV_INLINESTATS", "DEV_LOOKUP", "DEV_METRICS", "UNKNOWN_CMD", "LINE_COMMENT", - "MULTILINE_COMMENT", "WS", "COLON", "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", + "DEV_INLINESTATS", "DEV_LOOKUP", "DEV_METRICS", "DEV_JOIN", "DEV_JOIN_FULL", + "DEV_JOIN_LEFT", "DEV_JOIN_RIGHT", "DEV_JOIN_LOOKUP", "UNKNOWN_CMD", "LINE_COMMENT", + "MULTILINE_COMMENT", "WS", "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", "ASPERAND", "BACKQUOTE", "BACKQUOTE_BLOCK", "UNDERSCORE", "UNQUOTED_ID_BODY", "QUOTED_STRING", "INTEGER_LITERAL", - "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "CAST_OP", "COMMA", "DESC", - "DOT", "FALSE", "FIRST", "IN", "IS", "LAST", "LIKE", "LP", "NOT", "NULL", - "NULLS", "OR", "PARAM", "RLIKE", "RP", "TRUE", "EQ", "CIEQ", "NEQ", "LT", - "LTE", "GT", "GTE", "PLUS", "MINUS", "ASTERISK", "SLASH", "PERCENT", "EXPRESSION_COLON", + "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "CAST_OP", "COLON", "COMMA", + "DESC", "DOT", "FALSE", "FIRST", "IN", "IS", "LAST", "LIKE", "LP", "NOT", + "NULL", "NULLS", "OR", "PARAM", "RLIKE", "RP", "TRUE", "EQ", "CIEQ", "NEQ", + "LT", "LTE", "GT", "GTE", "PLUS", "MINUS", "ASTERISK", "SLASH", "PERCENT", "NESTED_WHERE", "NAMED_OR_POSITIONAL_PARAM", "OPENING_BRACKET", "CLOSING_BRACKET", "UNQUOTED_IDENTIFIER", "QUOTED_ID", "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", "EXPLAIN_OPENING_BRACKET", "EXPLAIN_PIPE", @@ -349,11 +379,14 @@ export default class esql_lexer extends lexer_config { "LOOKUP_UNQUOTED_SOURCE", "LOOKUP_QUOTED_SOURCE", "LOOKUP_LINE_COMMENT", "LOOKUP_MULTILINE_COMMENT", "LOOKUP_WS", "LOOKUP_FIELD_PIPE", "LOOKUP_FIELD_COMMA", "LOOKUP_FIELD_DOT", "LOOKUP_FIELD_ID_PATTERN", "LOOKUP_FIELD_LINE_COMMENT", - "LOOKUP_FIELD_MULTILINE_COMMENT", "LOOKUP_FIELD_WS", "METRICS_PIPE", "METRICS_UNQUOTED_SOURCE", - "METRICS_QUOTED_SOURCE", "METRICS_LINE_COMMENT", "METRICS_MULTILINE_COMMENT", - "METRICS_WS", "CLOSING_METRICS_COLON", "CLOSING_METRICS_COMMA", "CLOSING_METRICS_LINE_COMMENT", - "CLOSING_METRICS_MULTILINE_COMMENT", "CLOSING_METRICS_WS", "CLOSING_METRICS_QUOTED_IDENTIFIER", - "CLOSING_METRICS_UNQUOTED_IDENTIFIER", "CLOSING_METRICS_BY", "CLOSING_METRICS_PIPE", + "LOOKUP_FIELD_MULTILINE_COMMENT", "LOOKUP_FIELD_WS", "JOIN_PIPE", "JOIN_JOIN", + "JOIN_AS", "JOIN_ON", "USING", "JOIN_UNQUOTED_IDENTIFER", "JOIN_QUOTED_IDENTIFIER", + "JOIN_LINE_COMMENT", "JOIN_MULTILINE_COMMENT", "JOIN_WS", "METRICS_PIPE", + "METRICS_UNQUOTED_SOURCE", "METRICS_QUOTED_SOURCE", "METRICS_LINE_COMMENT", + "METRICS_MULTILINE_COMMENT", "METRICS_WS", "CLOSING_METRICS_COLON", "CLOSING_METRICS_COMMA", + "CLOSING_METRICS_LINE_COMMENT", "CLOSING_METRICS_MULTILINE_COMMENT", "CLOSING_METRICS_WS", + "CLOSING_METRICS_QUOTED_IDENTIFIER", "CLOSING_METRICS_UNQUOTED_IDENTIFIER", + "CLOSING_METRICS_BY", "CLOSING_METRICS_PIPE", ]; @@ -383,23 +416,31 @@ export default class esql_lexer extends lexer_config { return this.DEV_LOOKUP_sempred(localctx, predIndex); case 18: return this.DEV_METRICS_sempred(localctx, predIndex); - case 73: - return this.EXPRESSION_COLON_sempred(localctx, predIndex); - case 106: + case 19: + return this.DEV_JOIN_sempred(localctx, predIndex); + case 20: + return this.DEV_JOIN_FULL_sempred(localctx, predIndex); + case 21: + return this.DEV_JOIN_LEFT_sempred(localctx, predIndex); + case 22: + return this.DEV_JOIN_RIGHT_sempred(localctx, predIndex); + case 23: + return this.DEV_JOIN_LOOKUP_sempred(localctx, predIndex); + case 110: return this.PROJECT_PARAM_sempred(localctx, predIndex); - case 107: + case 111: return this.PROJECT_NAMED_OR_POSITIONAL_PARAM_sempred(localctx, predIndex); - case 118: + case 122: return this.RENAME_PARAM_sempred(localctx, predIndex); - case 119: + case 123: return this.RENAME_NAMED_OR_POSITIONAL_PARAM_sempred(localctx, predIndex); - case 142: + case 146: return this.ENRICH_FIELD_PARAM_sempred(localctx, predIndex); - case 143: + case 147: return this.ENRICH_FIELD_NAMED_OR_POSITIONAL_PARAM_sempred(localctx, predIndex); - case 149: + case 153: return this.MVEXPAND_PARAM_sempred(localctx, predIndex); - case 150: + case 154: return this.MVEXPAND_NAMED_OR_POSITIONAL_PARAM_sempred(localctx, predIndex); } return true; @@ -425,86 +466,114 @@ export default class esql_lexer extends lexer_config { } return true; } - private EXPRESSION_COLON_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 3: return this.isDevVersion(); } return true; } - private PROJECT_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_FULL_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 4: return this.isDevVersion(); } return true; } - private PROJECT_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_LEFT_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 5: return this.isDevVersion(); } return true; } - private RENAME_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_RIGHT_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 6: return this.isDevVersion(); } return true; } - private RENAME_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private DEV_JOIN_LOOKUP_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 7: return this.isDevVersion(); } return true; } - private ENRICH_FIELD_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private PROJECT_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 8: return this.isDevVersion(); } return true; } - private ENRICH_FIELD_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private PROJECT_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 9: return this.isDevVersion(); } return true; } - private MVEXPAND_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private RENAME_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 10: return this.isDevVersion(); } return true; } - private MVEXPAND_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + private RENAME_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { switch (predIndex) { case 11: return this.isDevVersion(); } return true; } + private ENRICH_FIELD_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 12: + return this.isDevVersion(); + } + return true; + } + private ENRICH_FIELD_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 13: + return this.isDevVersion(); + } + return true; + } + private MVEXPAND_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 14: + return this.isDevVersion(); + } + return true; + } + private MVEXPAND_NAMED_OR_POSITIONAL_PARAM_sempred(localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 15: + return this.isDevVersion(); + } + return true; + } - public static readonly _serializedATN: number[] = [4,0,119,1484,6,-1,6, - -1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,2,0, - 7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9, - 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7, - 16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23, - 2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2, - 31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38, - 7,38,2,39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7, - 45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7,52, - 2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2,59,7,59,2, - 60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2,65,7,65,2,66,7,66,2,67, - 7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7,71,2,72,7,72,2,73,7,73,2,74,7, - 74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7,78,2,79,7,79,2,80,7,80,2,81,7,81, - 2,82,7,82,2,83,7,83,2,84,7,84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2, - 89,7,89,2,90,7,90,2,91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96, - 7,96,2,97,7,97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102, + public static readonly _serializedATN: number[] = [4,0,128,1601,6,-1,6, + -1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1,6,-1, + 2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8, + 2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16, + 7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7, + 23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30, + 2,31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2, + 38,7,38,2,39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45, + 7,45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7, + 52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2,59,7,59, + 2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2,65,7,65,2,66,7,66,2, + 67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7,71,2,72,7,72,2,73,7,73,2,74, + 7,74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7,78,2,79,7,79,2,80,7,80,2,81,7, + 81,2,82,7,82,2,83,7,83,2,84,7,84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88, + 2,89,7,89,2,90,7,90,2,91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2, + 96,7,96,2,97,7,97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102, 2,103,7,103,2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108, 2,109,7,109,2,110,7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114,7,114, 2,115,7,115,2,116,7,116,2,117,7,117,2,118,7,118,2,119,7,119,2,120,7,120, @@ -521,484 +590,526 @@ export default class esql_lexer extends lexer_config { 2,181,7,181,2,182,7,182,2,183,7,183,2,184,7,184,2,185,7,185,2,186,7,186, 2,187,7,187,2,188,7,188,2,189,7,189,2,190,7,190,2,191,7,191,2,192,7,192, 2,193,7,193,2,194,7,194,2,195,7,195,2,196,7,196,2,197,7,197,2,198,7,198, - 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4, - 1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6, - 1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8, - 1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10, - 1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1, - 12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14, - 1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1, - 16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17, - 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1, - 18,1,18,1,18,1,18,1,19,4,19,580,8,19,11,19,12,19,581,1,19,1,19,1,20,1,20, - 1,20,1,20,5,20,590,8,20,10,20,12,20,593,9,20,1,20,3,20,596,8,20,1,20,3, - 20,599,8,20,1,20,1,20,1,21,1,21,1,21,1,21,1,21,5,21,608,8,21,10,21,12,21, - 611,9,21,1,21,1,21,1,21,1,21,1,21,1,22,4,22,619,8,22,11,22,12,22,620,1, - 22,1,22,1,23,1,23,1,24,1,24,1,24,1,24,1,25,1,25,1,26,1,26,1,27,1,27,1,27, - 1,28,1,28,1,29,1,29,3,29,642,8,29,1,29,4,29,645,8,29,11,29,12,29,646,1, - 30,1,30,1,31,1,31,1,32,1,32,1,32,3,32,656,8,32,1,33,1,33,1,34,1,34,1,34, - 3,34,663,8,34,1,35,1,35,1,35,5,35,668,8,35,10,35,12,35,671,9,35,1,35,1, - 35,1,35,1,35,1,35,1,35,5,35,679,8,35,10,35,12,35,682,9,35,1,35,1,35,1,35, - 1,35,1,35,3,35,689,8,35,1,35,3,35,692,8,35,3,35,694,8,35,1,36,4,36,697, - 8,36,11,36,12,36,698,1,37,4,37,702,8,37,11,37,12,37,703,1,37,1,37,5,37, - 708,8,37,10,37,12,37,711,9,37,1,37,1,37,4,37,715,8,37,11,37,12,37,716,1, - 37,4,37,720,8,37,11,37,12,37,721,1,37,1,37,5,37,726,8,37,10,37,12,37,729, - 9,37,3,37,731,8,37,1,37,1,37,1,37,1,37,4,37,737,8,37,11,37,12,37,738,1, - 37,1,37,3,37,743,8,37,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,40,1,40,1,40, - 1,40,1,41,1,41,1,42,1,42,1,42,1,43,1,43,1,44,1,44,1,44,1,44,1,44,1,45,1, - 45,1,46,1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,47,1,48,1,48, - 1,48,1,49,1,49,1,49,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1, - 52,1,52,1,53,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55, - 1,55,1,55,1,56,1,56,1,56,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,59,1, - 59,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,62,1,62,1,62,1,63,1,63,1,63, - 1,64,1,64,1,65,1,65,1,65,1,66,1,66,1,67,1,67,1,67,1,68,1,68,1,69,1,69,1, - 70,1,70,1,71,1,71,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74, - 1,75,1,75,1,75,3,75,874,8,75,1,75,5,75,877,8,75,10,75,12,75,880,9,75,1, - 75,1,75,4,75,884,8,75,11,75,12,75,885,3,75,888,8,75,1,76,1,76,1,76,1,76, - 1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,5,78,902,8,78,10,78,12,78,905,9, - 78,1,78,1,78,3,78,909,8,78,1,78,4,78,912,8,78,11,78,12,78,913,3,78,916, - 8,78,1,79,1,79,4,79,920,8,79,11,79,12,79,921,1,79,1,79,1,80,1,80,1,81,1, - 81,1,81,1,81,1,82,1,82,1,82,1,82,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84, - 1,84,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1,87,1,87,1,87,1,87,1, - 88,1,88,1,88,1,88,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,90,1,91,1,91, - 1,91,1,91,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1, - 95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,96,1,96,1,96,3,96,999,8,96, - 1,97,4,97,1002,8,97,11,97,12,97,1003,1,98,1,98,1,98,1,98,1,99,1,99,1,99, - 1,99,1,100,1,100,1,100,1,100,1,101,1,101,1,101,1,101,1,102,1,102,1,102, - 1,102,1,103,1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,105,1,105, - 1,105,1,105,1,106,1,106,1,106,1,106,1,106,1,107,1,107,1,107,1,107,1,107, - 1,108,1,108,1,108,1,108,3,108,1053,8,108,1,109,1,109,3,109,1057,8,109,1, - 109,5,109,1060,8,109,10,109,12,109,1063,9,109,1,109,1,109,3,109,1067,8, - 109,1,109,4,109,1070,8,109,11,109,12,109,1071,3,109,1074,8,109,1,110,1, - 110,4,110,1078,8,110,11,110,12,110,1079,1,111,1,111,1,111,1,111,1,112,1, - 112,1,112,1,112,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114,1, - 115,1,115,1,115,1,115,1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1, - 118,1,118,1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,120,1,120,1, - 120,1,121,1,121,1,121,1,121,1,122,1,122,1,122,1,122,1,123,1,123,1,123,1, - 123,1,124,1,124,1,124,1,124,1,125,1,125,1,125,1,125,1,125,1,126,1,126,1, - 126,1,126,1,126,1,127,1,127,1,127,1,127,1,127,1,128,1,128,1,128,1,128,1, - 128,1,128,1,128,1,129,1,129,1,130,4,130,1165,8,130,11,130,12,130,1166,1, - 130,1,130,3,130,1171,8,130,1,130,4,130,1174,8,130,11,130,12,130,1175,1, - 131,1,131,1,131,1,131,1,132,1,132,1,132,1,132,1,133,1,133,1,133,1,133,1, - 134,1,134,1,134,1,134,1,135,1,135,1,135,1,135,1,135,1,135,1,136,1,136,1, - 136,1,136,1,137,1,137,1,137,1,137,1,138,1,138,1,138,1,138,1,139,1,139,1, - 139,1,139,1,140,1,140,1,140,1,140,1,141,1,141,1,141,1,141,1,142,1,142,1, - 142,1,142,1,142,1,143,1,143,1,143,1,143,1,143,1,144,1,144,1,144,1,144,1, - 145,1,145,1,145,1,145,1,146,1,146,1,146,1,146,1,147,1,147,1,147,1,147,1, - 147,1,148,1,148,1,148,1,148,1,149,1,149,1,149,1,149,1,149,1,150,1,150,1, - 150,1,150,1,150,1,151,1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,153,1, - 153,1,153,1,153,1,154,1,154,1,154,1,154,1,155,1,155,1,155,1,155,1,156,1, - 156,1,156,1,156,1,156,1,157,1,157,1,157,1,157,1,157,1,158,1,158,1,158,1, - 158,1,159,1,159,1,159,1,159,1,160,1,160,1,160,1,160,1,161,1,161,1,161,1, - 161,1,161,1,162,1,162,1,162,1,162,1,163,1,163,1,163,1,163,1,163,4,163,1321, - 8,163,11,163,12,163,1322,1,164,1,164,1,164,1,164,1,165,1,165,1,165,1,165, - 1,166,1,166,1,166,1,166,1,167,1,167,1,167,1,167,1,167,1,168,1,168,1,168, - 1,168,1,169,1,169,1,169,1,169,1,170,1,170,1,170,1,170,1,171,1,171,1,171, - 1,171,1,171,1,172,1,172,1,172,1,172,1,173,1,173,1,173,1,173,1,174,1,174, - 1,174,1,174,1,175,1,175,1,175,1,175,1,176,1,176,1,176,1,176,1,177,1,177, - 1,177,1,177,1,177,1,177,1,178,1,178,1,178,1,178,1,179,1,179,1,179,1,179, - 1,180,1,180,1,180,1,180,1,181,1,181,1,181,1,181,1,182,1,182,1,182,1,182, - 1,183,1,183,1,183,1,183,1,184,1,184,1,184,1,184,1,184,1,185,1,185,1,185, - 1,185,1,185,1,185,1,186,1,186,1,186,1,186,1,186,1,186,1,187,1,187,1,187, - 1,187,1,188,1,188,1,188,1,188,1,189,1,189,1,189,1,189,1,190,1,190,1,190, - 1,190,1,190,1,190,1,191,1,191,1,191,1,191,1,191,1,191,1,192,1,192,1,192, - 1,192,1,193,1,193,1,193,1,193,1,194,1,194,1,194,1,194,1,195,1,195,1,195, - 1,195,1,195,1,195,1,196,1,196,1,196,1,196,1,196,1,196,1,197,1,197,1,197, - 1,197,1,197,1,197,1,198,1,198,1,198,1,198,1,198,2,609,680,0,199,15,1,17, - 2,19,3,21,4,23,5,25,6,27,7,29,8,31,9,33,10,35,11,37,12,39,13,41,14,43,15, - 45,16,47,17,49,18,51,19,53,20,55,21,57,22,59,23,61,24,63,25,65,0,67,0,69, - 0,71,0,73,0,75,0,77,0,79,0,81,0,83,0,85,26,87,27,89,28,91,29,93,30,95,31, - 97,32,99,33,101,34,103,35,105,36,107,37,109,38,111,39,113,40,115,41,117, - 42,119,43,121,44,123,45,125,46,127,47,129,48,131,49,133,50,135,51,137,52, - 139,53,141,54,143,55,145,56,147,57,149,58,151,59,153,60,155,61,157,62,159, - 63,161,0,163,0,165,64,167,65,169,66,171,67,173,0,175,68,177,69,179,70,181, - 71,183,0,185,0,187,72,189,73,191,74,193,0,195,0,197,0,199,0,201,0,203,0, - 205,75,207,0,209,76,211,0,213,0,215,77,217,78,219,79,221,0,223,0,225,0, - 227,0,229,0,231,0,233,0,235,80,237,81,239,82,241,83,243,0,245,0,247,0,249, - 0,251,0,253,0,255,84,257,0,259,85,261,86,263,87,265,0,267,0,269,88,271, - 89,273,0,275,90,277,0,279,91,281,92,283,93,285,0,287,0,289,0,291,0,293, - 0,295,0,297,0,299,0,301,0,303,94,305,95,307,96,309,0,311,0,313,0,315,0, - 317,0,319,0,321,97,323,98,325,99,327,0,329,100,331,101,333,102,335,103, - 337,0,339,0,341,104,343,105,345,106,347,107,349,0,351,0,353,0,355,0,357, - 0,359,0,361,0,363,108,365,109,367,110,369,0,371,0,373,0,375,0,377,111,379, - 112,381,113,383,0,385,0,387,0,389,114,391,115,393,116,395,0,397,0,399,117, - 401,118,403,119,405,0,407,0,409,0,411,0,15,0,1,2,3,4,5,6,7,8,9,10,11,12, - 13,14,35,2,0,68,68,100,100,2,0,73,73,105,105,2,0,83,83,115,115,2,0,69,69, - 101,101,2,0,67,67,99,99,2,0,84,84,116,116,2,0,82,82,114,114,2,0,79,79,111, - 111,2,0,80,80,112,112,2,0,78,78,110,110,2,0,72,72,104,104,2,0,86,86,118, - 118,2,0,65,65,97,97,2,0,76,76,108,108,2,0,88,88,120,120,2,0,70,70,102,102, - 2,0,77,77,109,109,2,0,71,71,103,103,2,0,75,75,107,107,2,0,87,87,119,119, - 2,0,85,85,117,117,6,0,9,10,13,13,32,32,47,47,91,91,93,93,2,0,10,10,13,13, - 3,0,9,10,13,13,32,32,1,0,48,57,2,0,65,90,97,122,8,0,34,34,78,78,82,82,84, - 84,92,92,110,110,114,114,116,116,4,0,10,10,13,13,34,34,92,92,2,0,43,43, - 45,45,1,0,96,96,2,0,66,66,98,98,2,0,89,89,121,121,11,0,9,10,13,13,32,32, - 34,34,44,44,47,47,58,58,61,61,91,91,93,93,124,124,2,0,42,42,47,47,11,0, - 9,10,13,13,32,32,34,35,44,44,47,47,58,58,60,60,62,63,92,92,124,124,1512, - 0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1, - 0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0, - 0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1, - 0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0, - 0,59,1,0,0,0,0,61,1,0,0,0,1,63,1,0,0,0,1,85,1,0,0,0,1,87,1,0,0,0,1,89,1, - 0,0,0,1,91,1,0,0,0,1,93,1,0,0,0,1,95,1,0,0,0,1,97,1,0,0,0,1,99,1,0,0,0, - 1,101,1,0,0,0,1,103,1,0,0,0,1,105,1,0,0,0,1,107,1,0,0,0,1,109,1,0,0,0,1, - 111,1,0,0,0,1,113,1,0,0,0,1,115,1,0,0,0,1,117,1,0,0,0,1,119,1,0,0,0,1,121, - 1,0,0,0,1,123,1,0,0,0,1,125,1,0,0,0,1,127,1,0,0,0,1,129,1,0,0,0,1,131,1, - 0,0,0,1,133,1,0,0,0,1,135,1,0,0,0,1,137,1,0,0,0,1,139,1,0,0,0,1,141,1,0, - 0,0,1,143,1,0,0,0,1,145,1,0,0,0,1,147,1,0,0,0,1,149,1,0,0,0,1,151,1,0,0, - 0,1,153,1,0,0,0,1,155,1,0,0,0,1,157,1,0,0,0,1,159,1,0,0,0,1,161,1,0,0,0, - 1,163,1,0,0,0,1,165,1,0,0,0,1,167,1,0,0,0,1,169,1,0,0,0,1,171,1,0,0,0,1, - 175,1,0,0,0,1,177,1,0,0,0,1,179,1,0,0,0,1,181,1,0,0,0,2,183,1,0,0,0,2,185, - 1,0,0,0,2,187,1,0,0,0,2,189,1,0,0,0,2,191,1,0,0,0,3,193,1,0,0,0,3,195,1, - 0,0,0,3,197,1,0,0,0,3,199,1,0,0,0,3,201,1,0,0,0,3,203,1,0,0,0,3,205,1,0, - 0,0,3,209,1,0,0,0,3,211,1,0,0,0,3,213,1,0,0,0,3,215,1,0,0,0,3,217,1,0,0, - 0,3,219,1,0,0,0,4,221,1,0,0,0,4,223,1,0,0,0,4,225,1,0,0,0,4,227,1,0,0,0, - 4,229,1,0,0,0,4,235,1,0,0,0,4,237,1,0,0,0,4,239,1,0,0,0,4,241,1,0,0,0,5, - 243,1,0,0,0,5,245,1,0,0,0,5,247,1,0,0,0,5,249,1,0,0,0,5,251,1,0,0,0,5,253, - 1,0,0,0,5,255,1,0,0,0,5,257,1,0,0,0,5,259,1,0,0,0,5,261,1,0,0,0,5,263,1, - 0,0,0,6,265,1,0,0,0,6,267,1,0,0,0,6,269,1,0,0,0,6,271,1,0,0,0,6,275,1,0, - 0,0,6,277,1,0,0,0,6,279,1,0,0,0,6,281,1,0,0,0,6,283,1,0,0,0,7,285,1,0,0, - 0,7,287,1,0,0,0,7,289,1,0,0,0,7,291,1,0,0,0,7,293,1,0,0,0,7,295,1,0,0,0, - 7,297,1,0,0,0,7,299,1,0,0,0,7,301,1,0,0,0,7,303,1,0,0,0,7,305,1,0,0,0,7, - 307,1,0,0,0,8,309,1,0,0,0,8,311,1,0,0,0,8,313,1,0,0,0,8,315,1,0,0,0,8,317, - 1,0,0,0,8,319,1,0,0,0,8,321,1,0,0,0,8,323,1,0,0,0,8,325,1,0,0,0,9,327,1, - 0,0,0,9,329,1,0,0,0,9,331,1,0,0,0,9,333,1,0,0,0,9,335,1,0,0,0,10,337,1, - 0,0,0,10,339,1,0,0,0,10,341,1,0,0,0,10,343,1,0,0,0,10,345,1,0,0,0,10,347, - 1,0,0,0,11,349,1,0,0,0,11,351,1,0,0,0,11,353,1,0,0,0,11,355,1,0,0,0,11, - 357,1,0,0,0,11,359,1,0,0,0,11,361,1,0,0,0,11,363,1,0,0,0,11,365,1,0,0,0, - 11,367,1,0,0,0,12,369,1,0,0,0,12,371,1,0,0,0,12,373,1,0,0,0,12,375,1,0, - 0,0,12,377,1,0,0,0,12,379,1,0,0,0,12,381,1,0,0,0,13,383,1,0,0,0,13,385, - 1,0,0,0,13,387,1,0,0,0,13,389,1,0,0,0,13,391,1,0,0,0,13,393,1,0,0,0,14, - 395,1,0,0,0,14,397,1,0,0,0,14,399,1,0,0,0,14,401,1,0,0,0,14,403,1,0,0,0, - 14,405,1,0,0,0,14,407,1,0,0,0,14,409,1,0,0,0,14,411,1,0,0,0,15,413,1,0, - 0,0,17,423,1,0,0,0,19,430,1,0,0,0,21,439,1,0,0,0,23,446,1,0,0,0,25,456, - 1,0,0,0,27,463,1,0,0,0,29,470,1,0,0,0,31,477,1,0,0,0,33,485,1,0,0,0,35, - 497,1,0,0,0,37,506,1,0,0,0,39,512,1,0,0,0,41,519,1,0,0,0,43,526,1,0,0,0, - 45,534,1,0,0,0,47,542,1,0,0,0,49,557,1,0,0,0,51,567,1,0,0,0,53,579,1,0, - 0,0,55,585,1,0,0,0,57,602,1,0,0,0,59,618,1,0,0,0,61,624,1,0,0,0,63,626, - 1,0,0,0,65,630,1,0,0,0,67,632,1,0,0,0,69,634,1,0,0,0,71,637,1,0,0,0,73, - 639,1,0,0,0,75,648,1,0,0,0,77,650,1,0,0,0,79,655,1,0,0,0,81,657,1,0,0,0, - 83,662,1,0,0,0,85,693,1,0,0,0,87,696,1,0,0,0,89,742,1,0,0,0,91,744,1,0, - 0,0,93,747,1,0,0,0,95,751,1,0,0,0,97,755,1,0,0,0,99,757,1,0,0,0,101,760, - 1,0,0,0,103,762,1,0,0,0,105,767,1,0,0,0,107,769,1,0,0,0,109,775,1,0,0,0, - 111,781,1,0,0,0,113,784,1,0,0,0,115,787,1,0,0,0,117,792,1,0,0,0,119,797, - 1,0,0,0,121,799,1,0,0,0,123,803,1,0,0,0,125,808,1,0,0,0,127,814,1,0,0,0, - 129,817,1,0,0,0,131,819,1,0,0,0,133,825,1,0,0,0,135,827,1,0,0,0,137,832, - 1,0,0,0,139,835,1,0,0,0,141,838,1,0,0,0,143,841,1,0,0,0,145,843,1,0,0,0, - 147,846,1,0,0,0,149,848,1,0,0,0,151,851,1,0,0,0,153,853,1,0,0,0,155,855, - 1,0,0,0,157,857,1,0,0,0,159,859,1,0,0,0,161,861,1,0,0,0,163,866,1,0,0,0, - 165,887,1,0,0,0,167,889,1,0,0,0,169,894,1,0,0,0,171,915,1,0,0,0,173,917, - 1,0,0,0,175,925,1,0,0,0,177,927,1,0,0,0,179,931,1,0,0,0,181,935,1,0,0,0, - 183,939,1,0,0,0,185,944,1,0,0,0,187,949,1,0,0,0,189,953,1,0,0,0,191,957, - 1,0,0,0,193,961,1,0,0,0,195,966,1,0,0,0,197,970,1,0,0,0,199,974,1,0,0,0, - 201,978,1,0,0,0,203,982,1,0,0,0,205,986,1,0,0,0,207,998,1,0,0,0,209,1001, - 1,0,0,0,211,1005,1,0,0,0,213,1009,1,0,0,0,215,1013,1,0,0,0,217,1017,1,0, - 0,0,219,1021,1,0,0,0,221,1025,1,0,0,0,223,1030,1,0,0,0,225,1034,1,0,0,0, - 227,1038,1,0,0,0,229,1043,1,0,0,0,231,1052,1,0,0,0,233,1073,1,0,0,0,235, - 1077,1,0,0,0,237,1081,1,0,0,0,239,1085,1,0,0,0,241,1089,1,0,0,0,243,1093, - 1,0,0,0,245,1098,1,0,0,0,247,1102,1,0,0,0,249,1106,1,0,0,0,251,1110,1,0, - 0,0,253,1115,1,0,0,0,255,1120,1,0,0,0,257,1123,1,0,0,0,259,1127,1,0,0,0, - 261,1131,1,0,0,0,263,1135,1,0,0,0,265,1139,1,0,0,0,267,1144,1,0,0,0,269, - 1149,1,0,0,0,271,1154,1,0,0,0,273,1161,1,0,0,0,275,1170,1,0,0,0,277,1177, - 1,0,0,0,279,1181,1,0,0,0,281,1185,1,0,0,0,283,1189,1,0,0,0,285,1193,1,0, - 0,0,287,1199,1,0,0,0,289,1203,1,0,0,0,291,1207,1,0,0,0,293,1211,1,0,0,0, - 295,1215,1,0,0,0,297,1219,1,0,0,0,299,1223,1,0,0,0,301,1228,1,0,0,0,303, - 1233,1,0,0,0,305,1237,1,0,0,0,307,1241,1,0,0,0,309,1245,1,0,0,0,311,1250, - 1,0,0,0,313,1254,1,0,0,0,315,1259,1,0,0,0,317,1264,1,0,0,0,319,1268,1,0, - 0,0,321,1272,1,0,0,0,323,1276,1,0,0,0,325,1280,1,0,0,0,327,1284,1,0,0,0, - 329,1289,1,0,0,0,331,1294,1,0,0,0,333,1298,1,0,0,0,335,1302,1,0,0,0,337, - 1306,1,0,0,0,339,1311,1,0,0,0,341,1320,1,0,0,0,343,1324,1,0,0,0,345,1328, - 1,0,0,0,347,1332,1,0,0,0,349,1336,1,0,0,0,351,1341,1,0,0,0,353,1345,1,0, - 0,0,355,1349,1,0,0,0,357,1353,1,0,0,0,359,1358,1,0,0,0,361,1362,1,0,0,0, - 363,1366,1,0,0,0,365,1370,1,0,0,0,367,1374,1,0,0,0,369,1378,1,0,0,0,371, - 1384,1,0,0,0,373,1388,1,0,0,0,375,1392,1,0,0,0,377,1396,1,0,0,0,379,1400, - 1,0,0,0,381,1404,1,0,0,0,383,1408,1,0,0,0,385,1413,1,0,0,0,387,1419,1,0, - 0,0,389,1425,1,0,0,0,391,1429,1,0,0,0,393,1433,1,0,0,0,395,1437,1,0,0,0, - 397,1443,1,0,0,0,399,1449,1,0,0,0,401,1453,1,0,0,0,403,1457,1,0,0,0,405, - 1461,1,0,0,0,407,1467,1,0,0,0,409,1473,1,0,0,0,411,1479,1,0,0,0,413,414, - 7,0,0,0,414,415,7,1,0,0,415,416,7,2,0,0,416,417,7,2,0,0,417,418,7,3,0,0, - 418,419,7,4,0,0,419,420,7,5,0,0,420,421,1,0,0,0,421,422,6,0,0,0,422,16, - 1,0,0,0,423,424,7,0,0,0,424,425,7,6,0,0,425,426,7,7,0,0,426,427,7,8,0,0, - 427,428,1,0,0,0,428,429,6,1,1,0,429,18,1,0,0,0,430,431,7,3,0,0,431,432, - 7,9,0,0,432,433,7,6,0,0,433,434,7,1,0,0,434,435,7,4,0,0,435,436,7,10,0, - 0,436,437,1,0,0,0,437,438,6,2,2,0,438,20,1,0,0,0,439,440,7,3,0,0,440,441, - 7,11,0,0,441,442,7,12,0,0,442,443,7,13,0,0,443,444,1,0,0,0,444,445,6,3, - 0,0,445,22,1,0,0,0,446,447,7,3,0,0,447,448,7,14,0,0,448,449,7,8,0,0,449, - 450,7,13,0,0,450,451,7,12,0,0,451,452,7,1,0,0,452,453,7,9,0,0,453,454,1, - 0,0,0,454,455,6,4,3,0,455,24,1,0,0,0,456,457,7,15,0,0,457,458,7,6,0,0,458, - 459,7,7,0,0,459,460,7,16,0,0,460,461,1,0,0,0,461,462,6,5,4,0,462,26,1,0, - 0,0,463,464,7,17,0,0,464,465,7,6,0,0,465,466,7,7,0,0,466,467,7,18,0,0,467, - 468,1,0,0,0,468,469,6,6,0,0,469,28,1,0,0,0,470,471,7,18,0,0,471,472,7,3, - 0,0,472,473,7,3,0,0,473,474,7,8,0,0,474,475,1,0,0,0,475,476,6,7,1,0,476, - 30,1,0,0,0,477,478,7,13,0,0,478,479,7,1,0,0,479,480,7,16,0,0,480,481,7, - 1,0,0,481,482,7,5,0,0,482,483,1,0,0,0,483,484,6,8,0,0,484,32,1,0,0,0,485, - 486,7,16,0,0,486,487,7,11,0,0,487,488,5,95,0,0,488,489,7,3,0,0,489,490, - 7,14,0,0,490,491,7,8,0,0,491,492,7,12,0,0,492,493,7,9,0,0,493,494,7,0,0, - 0,494,495,1,0,0,0,495,496,6,9,5,0,496,34,1,0,0,0,497,498,7,6,0,0,498,499, - 7,3,0,0,499,500,7,9,0,0,500,501,7,12,0,0,501,502,7,16,0,0,502,503,7,3,0, - 0,503,504,1,0,0,0,504,505,6,10,6,0,505,36,1,0,0,0,506,507,7,6,0,0,507,508, - 7,7,0,0,508,509,7,19,0,0,509,510,1,0,0,0,510,511,6,11,0,0,511,38,1,0,0, - 0,512,513,7,2,0,0,513,514,7,10,0,0,514,515,7,7,0,0,515,516,7,19,0,0,516, - 517,1,0,0,0,517,518,6,12,7,0,518,40,1,0,0,0,519,520,7,2,0,0,520,521,7,7, - 0,0,521,522,7,6,0,0,522,523,7,5,0,0,523,524,1,0,0,0,524,525,6,13,0,0,525, - 42,1,0,0,0,526,527,7,2,0,0,527,528,7,5,0,0,528,529,7,12,0,0,529,530,7,5, - 0,0,530,531,7,2,0,0,531,532,1,0,0,0,532,533,6,14,0,0,533,44,1,0,0,0,534, - 535,7,19,0,0,535,536,7,10,0,0,536,537,7,3,0,0,537,538,7,6,0,0,538,539,7, - 3,0,0,539,540,1,0,0,0,540,541,6,15,0,0,541,46,1,0,0,0,542,543,4,16,0,0, - 543,544,7,1,0,0,544,545,7,9,0,0,545,546,7,13,0,0,546,547,7,1,0,0,547,548, - 7,9,0,0,548,549,7,3,0,0,549,550,7,2,0,0,550,551,7,5,0,0,551,552,7,12,0, - 0,552,553,7,5,0,0,553,554,7,2,0,0,554,555,1,0,0,0,555,556,6,16,0,0,556, - 48,1,0,0,0,557,558,4,17,1,0,558,559,7,13,0,0,559,560,7,7,0,0,560,561,7, - 7,0,0,561,562,7,18,0,0,562,563,7,20,0,0,563,564,7,8,0,0,564,565,1,0,0,0, - 565,566,6,17,8,0,566,50,1,0,0,0,567,568,4,18,2,0,568,569,7,16,0,0,569,570, - 7,3,0,0,570,571,7,5,0,0,571,572,7,6,0,0,572,573,7,1,0,0,573,574,7,4,0,0, - 574,575,7,2,0,0,575,576,1,0,0,0,576,577,6,18,9,0,577,52,1,0,0,0,578,580, - 8,21,0,0,579,578,1,0,0,0,580,581,1,0,0,0,581,579,1,0,0,0,581,582,1,0,0, - 0,582,583,1,0,0,0,583,584,6,19,0,0,584,54,1,0,0,0,585,586,5,47,0,0,586, - 587,5,47,0,0,587,591,1,0,0,0,588,590,8,22,0,0,589,588,1,0,0,0,590,593,1, - 0,0,0,591,589,1,0,0,0,591,592,1,0,0,0,592,595,1,0,0,0,593,591,1,0,0,0,594, - 596,5,13,0,0,595,594,1,0,0,0,595,596,1,0,0,0,596,598,1,0,0,0,597,599,5, - 10,0,0,598,597,1,0,0,0,598,599,1,0,0,0,599,600,1,0,0,0,600,601,6,20,10, - 0,601,56,1,0,0,0,602,603,5,47,0,0,603,604,5,42,0,0,604,609,1,0,0,0,605, - 608,3,57,21,0,606,608,9,0,0,0,607,605,1,0,0,0,607,606,1,0,0,0,608,611,1, - 0,0,0,609,610,1,0,0,0,609,607,1,0,0,0,610,612,1,0,0,0,611,609,1,0,0,0,612, - 613,5,42,0,0,613,614,5,47,0,0,614,615,1,0,0,0,615,616,6,21,10,0,616,58, - 1,0,0,0,617,619,7,23,0,0,618,617,1,0,0,0,619,620,1,0,0,0,620,618,1,0,0, - 0,620,621,1,0,0,0,621,622,1,0,0,0,622,623,6,22,10,0,623,60,1,0,0,0,624, - 625,5,58,0,0,625,62,1,0,0,0,626,627,5,124,0,0,627,628,1,0,0,0,628,629,6, - 24,11,0,629,64,1,0,0,0,630,631,7,24,0,0,631,66,1,0,0,0,632,633,7,25,0,0, - 633,68,1,0,0,0,634,635,5,92,0,0,635,636,7,26,0,0,636,70,1,0,0,0,637,638, - 8,27,0,0,638,72,1,0,0,0,639,641,7,3,0,0,640,642,7,28,0,0,641,640,1,0,0, - 0,641,642,1,0,0,0,642,644,1,0,0,0,643,645,3,65,25,0,644,643,1,0,0,0,645, - 646,1,0,0,0,646,644,1,0,0,0,646,647,1,0,0,0,647,74,1,0,0,0,648,649,5,64, - 0,0,649,76,1,0,0,0,650,651,5,96,0,0,651,78,1,0,0,0,652,656,8,29,0,0,653, - 654,5,96,0,0,654,656,5,96,0,0,655,652,1,0,0,0,655,653,1,0,0,0,656,80,1, - 0,0,0,657,658,5,95,0,0,658,82,1,0,0,0,659,663,3,67,26,0,660,663,3,65,25, - 0,661,663,3,81,33,0,662,659,1,0,0,0,662,660,1,0,0,0,662,661,1,0,0,0,663, - 84,1,0,0,0,664,669,5,34,0,0,665,668,3,69,27,0,666,668,3,71,28,0,667,665, - 1,0,0,0,667,666,1,0,0,0,668,671,1,0,0,0,669,667,1,0,0,0,669,670,1,0,0,0, - 670,672,1,0,0,0,671,669,1,0,0,0,672,694,5,34,0,0,673,674,5,34,0,0,674,675, - 5,34,0,0,675,676,5,34,0,0,676,680,1,0,0,0,677,679,8,22,0,0,678,677,1,0, - 0,0,679,682,1,0,0,0,680,681,1,0,0,0,680,678,1,0,0,0,681,683,1,0,0,0,682, - 680,1,0,0,0,683,684,5,34,0,0,684,685,5,34,0,0,685,686,5,34,0,0,686,688, - 1,0,0,0,687,689,5,34,0,0,688,687,1,0,0,0,688,689,1,0,0,0,689,691,1,0,0, - 0,690,692,5,34,0,0,691,690,1,0,0,0,691,692,1,0,0,0,692,694,1,0,0,0,693, - 664,1,0,0,0,693,673,1,0,0,0,694,86,1,0,0,0,695,697,3,65,25,0,696,695,1, - 0,0,0,697,698,1,0,0,0,698,696,1,0,0,0,698,699,1,0,0,0,699,88,1,0,0,0,700, - 702,3,65,25,0,701,700,1,0,0,0,702,703,1,0,0,0,703,701,1,0,0,0,703,704,1, - 0,0,0,704,705,1,0,0,0,705,709,3,105,45,0,706,708,3,65,25,0,707,706,1,0, - 0,0,708,711,1,0,0,0,709,707,1,0,0,0,709,710,1,0,0,0,710,743,1,0,0,0,711, - 709,1,0,0,0,712,714,3,105,45,0,713,715,3,65,25,0,714,713,1,0,0,0,715,716, - 1,0,0,0,716,714,1,0,0,0,716,717,1,0,0,0,717,743,1,0,0,0,718,720,3,65,25, - 0,719,718,1,0,0,0,720,721,1,0,0,0,721,719,1,0,0,0,721,722,1,0,0,0,722,730, - 1,0,0,0,723,727,3,105,45,0,724,726,3,65,25,0,725,724,1,0,0,0,726,729,1, - 0,0,0,727,725,1,0,0,0,727,728,1,0,0,0,728,731,1,0,0,0,729,727,1,0,0,0,730, - 723,1,0,0,0,730,731,1,0,0,0,731,732,1,0,0,0,732,733,3,73,29,0,733,743,1, - 0,0,0,734,736,3,105,45,0,735,737,3,65,25,0,736,735,1,0,0,0,737,738,1,0, - 0,0,738,736,1,0,0,0,738,739,1,0,0,0,739,740,1,0,0,0,740,741,3,73,29,0,741, - 743,1,0,0,0,742,701,1,0,0,0,742,712,1,0,0,0,742,719,1,0,0,0,742,734,1,0, - 0,0,743,90,1,0,0,0,744,745,7,30,0,0,745,746,7,31,0,0,746,92,1,0,0,0,747, - 748,7,12,0,0,748,749,7,9,0,0,749,750,7,0,0,0,750,94,1,0,0,0,751,752,7,12, - 0,0,752,753,7,2,0,0,753,754,7,4,0,0,754,96,1,0,0,0,755,756,5,61,0,0,756, - 98,1,0,0,0,757,758,5,58,0,0,758,759,5,58,0,0,759,100,1,0,0,0,760,761,5, - 44,0,0,761,102,1,0,0,0,762,763,7,0,0,0,763,764,7,3,0,0,764,765,7,2,0,0, - 765,766,7,4,0,0,766,104,1,0,0,0,767,768,5,46,0,0,768,106,1,0,0,0,769,770, - 7,15,0,0,770,771,7,12,0,0,771,772,7,13,0,0,772,773,7,2,0,0,773,774,7,3, - 0,0,774,108,1,0,0,0,775,776,7,15,0,0,776,777,7,1,0,0,777,778,7,6,0,0,778, - 779,7,2,0,0,779,780,7,5,0,0,780,110,1,0,0,0,781,782,7,1,0,0,782,783,7,9, - 0,0,783,112,1,0,0,0,784,785,7,1,0,0,785,786,7,2,0,0,786,114,1,0,0,0,787, - 788,7,13,0,0,788,789,7,12,0,0,789,790,7,2,0,0,790,791,7,5,0,0,791,116,1, - 0,0,0,792,793,7,13,0,0,793,794,7,1,0,0,794,795,7,18,0,0,795,796,7,3,0,0, - 796,118,1,0,0,0,797,798,5,40,0,0,798,120,1,0,0,0,799,800,7,9,0,0,800,801, - 7,7,0,0,801,802,7,5,0,0,802,122,1,0,0,0,803,804,7,9,0,0,804,805,7,20,0, - 0,805,806,7,13,0,0,806,807,7,13,0,0,807,124,1,0,0,0,808,809,7,9,0,0,809, - 810,7,20,0,0,810,811,7,13,0,0,811,812,7,13,0,0,812,813,7,2,0,0,813,126, - 1,0,0,0,814,815,7,7,0,0,815,816,7,6,0,0,816,128,1,0,0,0,817,818,5,63,0, - 0,818,130,1,0,0,0,819,820,7,6,0,0,820,821,7,13,0,0,821,822,7,1,0,0,822, - 823,7,18,0,0,823,824,7,3,0,0,824,132,1,0,0,0,825,826,5,41,0,0,826,134,1, - 0,0,0,827,828,7,5,0,0,828,829,7,6,0,0,829,830,7,20,0,0,830,831,7,3,0,0, - 831,136,1,0,0,0,832,833,5,61,0,0,833,834,5,61,0,0,834,138,1,0,0,0,835,836, - 5,61,0,0,836,837,5,126,0,0,837,140,1,0,0,0,838,839,5,33,0,0,839,840,5,61, - 0,0,840,142,1,0,0,0,841,842,5,60,0,0,842,144,1,0,0,0,843,844,5,60,0,0,844, - 845,5,61,0,0,845,146,1,0,0,0,846,847,5,62,0,0,847,148,1,0,0,0,848,849,5, - 62,0,0,849,850,5,61,0,0,850,150,1,0,0,0,851,852,5,43,0,0,852,152,1,0,0, - 0,853,854,5,45,0,0,854,154,1,0,0,0,855,856,5,42,0,0,856,156,1,0,0,0,857, - 858,5,47,0,0,858,158,1,0,0,0,859,860,5,37,0,0,860,160,1,0,0,0,861,862,4, - 73,3,0,862,863,3,61,23,0,863,864,1,0,0,0,864,865,6,73,12,0,865,162,1,0, - 0,0,866,867,3,45,15,0,867,868,1,0,0,0,868,869,6,74,13,0,869,164,1,0,0,0, - 870,873,3,129,57,0,871,874,3,67,26,0,872,874,3,81,33,0,873,871,1,0,0,0, - 873,872,1,0,0,0,874,878,1,0,0,0,875,877,3,83,34,0,876,875,1,0,0,0,877,880, - 1,0,0,0,878,876,1,0,0,0,878,879,1,0,0,0,879,888,1,0,0,0,880,878,1,0,0,0, - 881,883,3,129,57,0,882,884,3,65,25,0,883,882,1,0,0,0,884,885,1,0,0,0,885, - 883,1,0,0,0,885,886,1,0,0,0,886,888,1,0,0,0,887,870,1,0,0,0,887,881,1,0, - 0,0,888,166,1,0,0,0,889,890,5,91,0,0,890,891,1,0,0,0,891,892,6,76,0,0,892, - 893,6,76,0,0,893,168,1,0,0,0,894,895,5,93,0,0,895,896,1,0,0,0,896,897,6, - 77,11,0,897,898,6,77,11,0,898,170,1,0,0,0,899,903,3,67,26,0,900,902,3,83, - 34,0,901,900,1,0,0,0,902,905,1,0,0,0,903,901,1,0,0,0,903,904,1,0,0,0,904, - 916,1,0,0,0,905,903,1,0,0,0,906,909,3,81,33,0,907,909,3,75,30,0,908,906, - 1,0,0,0,908,907,1,0,0,0,909,911,1,0,0,0,910,912,3,83,34,0,911,910,1,0,0, - 0,912,913,1,0,0,0,913,911,1,0,0,0,913,914,1,0,0,0,914,916,1,0,0,0,915,899, - 1,0,0,0,915,908,1,0,0,0,916,172,1,0,0,0,917,919,3,77,31,0,918,920,3,79, - 32,0,919,918,1,0,0,0,920,921,1,0,0,0,921,919,1,0,0,0,921,922,1,0,0,0,922, - 923,1,0,0,0,923,924,3,77,31,0,924,174,1,0,0,0,925,926,3,173,79,0,926,176, - 1,0,0,0,927,928,3,55,20,0,928,929,1,0,0,0,929,930,6,81,10,0,930,178,1,0, - 0,0,931,932,3,57,21,0,932,933,1,0,0,0,933,934,6,82,10,0,934,180,1,0,0,0, - 935,936,3,59,22,0,936,937,1,0,0,0,937,938,6,83,10,0,938,182,1,0,0,0,939, - 940,3,167,76,0,940,941,1,0,0,0,941,942,6,84,14,0,942,943,6,84,15,0,943, - 184,1,0,0,0,944,945,3,63,24,0,945,946,1,0,0,0,946,947,6,85,16,0,947,948, - 6,85,11,0,948,186,1,0,0,0,949,950,3,59,22,0,950,951,1,0,0,0,951,952,6,86, - 10,0,952,188,1,0,0,0,953,954,3,55,20,0,954,955,1,0,0,0,955,956,6,87,10, - 0,956,190,1,0,0,0,957,958,3,57,21,0,958,959,1,0,0,0,959,960,6,88,10,0,960, - 192,1,0,0,0,961,962,3,63,24,0,962,963,1,0,0,0,963,964,6,89,16,0,964,965, - 6,89,11,0,965,194,1,0,0,0,966,967,3,167,76,0,967,968,1,0,0,0,968,969,6, - 90,14,0,969,196,1,0,0,0,970,971,3,169,77,0,971,972,1,0,0,0,972,973,6,91, - 17,0,973,198,1,0,0,0,974,975,3,61,23,0,975,976,1,0,0,0,976,977,6,92,12, - 0,977,200,1,0,0,0,978,979,3,101,43,0,979,980,1,0,0,0,980,981,6,93,18,0, - 981,202,1,0,0,0,982,983,3,97,41,0,983,984,1,0,0,0,984,985,6,94,19,0,985, - 204,1,0,0,0,986,987,7,16,0,0,987,988,7,3,0,0,988,989,7,5,0,0,989,990,7, - 12,0,0,990,991,7,0,0,0,991,992,7,12,0,0,992,993,7,5,0,0,993,994,7,12,0, - 0,994,206,1,0,0,0,995,999,8,32,0,0,996,997,5,47,0,0,997,999,8,33,0,0,998, - 995,1,0,0,0,998,996,1,0,0,0,999,208,1,0,0,0,1000,1002,3,207,96,0,1001,1000, - 1,0,0,0,1002,1003,1,0,0,0,1003,1001,1,0,0,0,1003,1004,1,0,0,0,1004,210, - 1,0,0,0,1005,1006,3,209,97,0,1006,1007,1,0,0,0,1007,1008,6,98,20,0,1008, - 212,1,0,0,0,1009,1010,3,85,35,0,1010,1011,1,0,0,0,1011,1012,6,99,21,0,1012, - 214,1,0,0,0,1013,1014,3,55,20,0,1014,1015,1,0,0,0,1015,1016,6,100,10,0, - 1016,216,1,0,0,0,1017,1018,3,57,21,0,1018,1019,1,0,0,0,1019,1020,6,101, - 10,0,1020,218,1,0,0,0,1021,1022,3,59,22,0,1022,1023,1,0,0,0,1023,1024,6, - 102,10,0,1024,220,1,0,0,0,1025,1026,3,63,24,0,1026,1027,1,0,0,0,1027,1028, - 6,103,16,0,1028,1029,6,103,11,0,1029,222,1,0,0,0,1030,1031,3,105,45,0,1031, - 1032,1,0,0,0,1032,1033,6,104,22,0,1033,224,1,0,0,0,1034,1035,3,101,43,0, - 1035,1036,1,0,0,0,1036,1037,6,105,18,0,1037,226,1,0,0,0,1038,1039,4,106, - 4,0,1039,1040,3,129,57,0,1040,1041,1,0,0,0,1041,1042,6,106,23,0,1042,228, - 1,0,0,0,1043,1044,4,107,5,0,1044,1045,3,165,75,0,1045,1046,1,0,0,0,1046, - 1047,6,107,24,0,1047,230,1,0,0,0,1048,1053,3,67,26,0,1049,1053,3,65,25, - 0,1050,1053,3,81,33,0,1051,1053,3,155,70,0,1052,1048,1,0,0,0,1052,1049, - 1,0,0,0,1052,1050,1,0,0,0,1052,1051,1,0,0,0,1053,232,1,0,0,0,1054,1057, - 3,67,26,0,1055,1057,3,155,70,0,1056,1054,1,0,0,0,1056,1055,1,0,0,0,1057, - 1061,1,0,0,0,1058,1060,3,231,108,0,1059,1058,1,0,0,0,1060,1063,1,0,0,0, - 1061,1059,1,0,0,0,1061,1062,1,0,0,0,1062,1074,1,0,0,0,1063,1061,1,0,0,0, - 1064,1067,3,81,33,0,1065,1067,3,75,30,0,1066,1064,1,0,0,0,1066,1065,1,0, - 0,0,1067,1069,1,0,0,0,1068,1070,3,231,108,0,1069,1068,1,0,0,0,1070,1071, - 1,0,0,0,1071,1069,1,0,0,0,1071,1072,1,0,0,0,1072,1074,1,0,0,0,1073,1056, - 1,0,0,0,1073,1066,1,0,0,0,1074,234,1,0,0,0,1075,1078,3,233,109,0,1076,1078, - 3,173,79,0,1077,1075,1,0,0,0,1077,1076,1,0,0,0,1078,1079,1,0,0,0,1079,1077, - 1,0,0,0,1079,1080,1,0,0,0,1080,236,1,0,0,0,1081,1082,3,55,20,0,1082,1083, - 1,0,0,0,1083,1084,6,111,10,0,1084,238,1,0,0,0,1085,1086,3,57,21,0,1086, - 1087,1,0,0,0,1087,1088,6,112,10,0,1088,240,1,0,0,0,1089,1090,3,59,22,0, - 1090,1091,1,0,0,0,1091,1092,6,113,10,0,1092,242,1,0,0,0,1093,1094,3,63, - 24,0,1094,1095,1,0,0,0,1095,1096,6,114,16,0,1096,1097,6,114,11,0,1097,244, - 1,0,0,0,1098,1099,3,97,41,0,1099,1100,1,0,0,0,1100,1101,6,115,19,0,1101, - 246,1,0,0,0,1102,1103,3,101,43,0,1103,1104,1,0,0,0,1104,1105,6,116,18,0, - 1105,248,1,0,0,0,1106,1107,3,105,45,0,1107,1108,1,0,0,0,1108,1109,6,117, - 22,0,1109,250,1,0,0,0,1110,1111,4,118,6,0,1111,1112,3,129,57,0,1112,1113, - 1,0,0,0,1113,1114,6,118,23,0,1114,252,1,0,0,0,1115,1116,4,119,7,0,1116, - 1117,3,165,75,0,1117,1118,1,0,0,0,1118,1119,6,119,24,0,1119,254,1,0,0,0, - 1120,1121,7,12,0,0,1121,1122,7,2,0,0,1122,256,1,0,0,0,1123,1124,3,235,110, - 0,1124,1125,1,0,0,0,1125,1126,6,121,25,0,1126,258,1,0,0,0,1127,1128,3,55, - 20,0,1128,1129,1,0,0,0,1129,1130,6,122,10,0,1130,260,1,0,0,0,1131,1132, - 3,57,21,0,1132,1133,1,0,0,0,1133,1134,6,123,10,0,1134,262,1,0,0,0,1135, - 1136,3,59,22,0,1136,1137,1,0,0,0,1137,1138,6,124,10,0,1138,264,1,0,0,0, - 1139,1140,3,63,24,0,1140,1141,1,0,0,0,1141,1142,6,125,16,0,1142,1143,6, - 125,11,0,1143,266,1,0,0,0,1144,1145,3,167,76,0,1145,1146,1,0,0,0,1146,1147, - 6,126,14,0,1147,1148,6,126,26,0,1148,268,1,0,0,0,1149,1150,7,7,0,0,1150, - 1151,7,9,0,0,1151,1152,1,0,0,0,1152,1153,6,127,27,0,1153,270,1,0,0,0,1154, - 1155,7,19,0,0,1155,1156,7,1,0,0,1156,1157,7,5,0,0,1157,1158,7,10,0,0,1158, - 1159,1,0,0,0,1159,1160,6,128,27,0,1160,272,1,0,0,0,1161,1162,8,34,0,0,1162, - 274,1,0,0,0,1163,1165,3,273,129,0,1164,1163,1,0,0,0,1165,1166,1,0,0,0,1166, - 1164,1,0,0,0,1166,1167,1,0,0,0,1167,1168,1,0,0,0,1168,1169,3,61,23,0,1169, - 1171,1,0,0,0,1170,1164,1,0,0,0,1170,1171,1,0,0,0,1171,1173,1,0,0,0,1172, - 1174,3,273,129,0,1173,1172,1,0,0,0,1174,1175,1,0,0,0,1175,1173,1,0,0,0, - 1175,1176,1,0,0,0,1176,276,1,0,0,0,1177,1178,3,275,130,0,1178,1179,1,0, - 0,0,1179,1180,6,131,28,0,1180,278,1,0,0,0,1181,1182,3,55,20,0,1182,1183, - 1,0,0,0,1183,1184,6,132,10,0,1184,280,1,0,0,0,1185,1186,3,57,21,0,1186, - 1187,1,0,0,0,1187,1188,6,133,10,0,1188,282,1,0,0,0,1189,1190,3,59,22,0, - 1190,1191,1,0,0,0,1191,1192,6,134,10,0,1192,284,1,0,0,0,1193,1194,3,63, - 24,0,1194,1195,1,0,0,0,1195,1196,6,135,16,0,1196,1197,6,135,11,0,1197,1198, - 6,135,11,0,1198,286,1,0,0,0,1199,1200,3,97,41,0,1200,1201,1,0,0,0,1201, - 1202,6,136,19,0,1202,288,1,0,0,0,1203,1204,3,101,43,0,1204,1205,1,0,0,0, - 1205,1206,6,137,18,0,1206,290,1,0,0,0,1207,1208,3,105,45,0,1208,1209,1, - 0,0,0,1209,1210,6,138,22,0,1210,292,1,0,0,0,1211,1212,3,271,128,0,1212, - 1213,1,0,0,0,1213,1214,6,139,29,0,1214,294,1,0,0,0,1215,1216,3,235,110, - 0,1216,1217,1,0,0,0,1217,1218,6,140,25,0,1218,296,1,0,0,0,1219,1220,3,175, - 80,0,1220,1221,1,0,0,0,1221,1222,6,141,30,0,1222,298,1,0,0,0,1223,1224, - 4,142,8,0,1224,1225,3,129,57,0,1225,1226,1,0,0,0,1226,1227,6,142,23,0,1227, - 300,1,0,0,0,1228,1229,4,143,9,0,1229,1230,3,165,75,0,1230,1231,1,0,0,0, - 1231,1232,6,143,24,0,1232,302,1,0,0,0,1233,1234,3,55,20,0,1234,1235,1,0, - 0,0,1235,1236,6,144,10,0,1236,304,1,0,0,0,1237,1238,3,57,21,0,1238,1239, - 1,0,0,0,1239,1240,6,145,10,0,1240,306,1,0,0,0,1241,1242,3,59,22,0,1242, - 1243,1,0,0,0,1243,1244,6,146,10,0,1244,308,1,0,0,0,1245,1246,3,63,24,0, - 1246,1247,1,0,0,0,1247,1248,6,147,16,0,1248,1249,6,147,11,0,1249,310,1, - 0,0,0,1250,1251,3,105,45,0,1251,1252,1,0,0,0,1252,1253,6,148,22,0,1253, - 312,1,0,0,0,1254,1255,4,149,10,0,1255,1256,3,129,57,0,1256,1257,1,0,0,0, - 1257,1258,6,149,23,0,1258,314,1,0,0,0,1259,1260,4,150,11,0,1260,1261,3, - 165,75,0,1261,1262,1,0,0,0,1262,1263,6,150,24,0,1263,316,1,0,0,0,1264,1265, - 3,175,80,0,1265,1266,1,0,0,0,1266,1267,6,151,30,0,1267,318,1,0,0,0,1268, - 1269,3,171,78,0,1269,1270,1,0,0,0,1270,1271,6,152,31,0,1271,320,1,0,0,0, - 1272,1273,3,55,20,0,1273,1274,1,0,0,0,1274,1275,6,153,10,0,1275,322,1,0, - 0,0,1276,1277,3,57,21,0,1277,1278,1,0,0,0,1278,1279,6,154,10,0,1279,324, - 1,0,0,0,1280,1281,3,59,22,0,1281,1282,1,0,0,0,1282,1283,6,155,10,0,1283, - 326,1,0,0,0,1284,1285,3,63,24,0,1285,1286,1,0,0,0,1286,1287,6,156,16,0, - 1287,1288,6,156,11,0,1288,328,1,0,0,0,1289,1290,7,1,0,0,1290,1291,7,9,0, - 0,1291,1292,7,15,0,0,1292,1293,7,7,0,0,1293,330,1,0,0,0,1294,1295,3,55, - 20,0,1295,1296,1,0,0,0,1296,1297,6,158,10,0,1297,332,1,0,0,0,1298,1299, - 3,57,21,0,1299,1300,1,0,0,0,1300,1301,6,159,10,0,1301,334,1,0,0,0,1302, - 1303,3,59,22,0,1303,1304,1,0,0,0,1304,1305,6,160,10,0,1305,336,1,0,0,0, - 1306,1307,3,169,77,0,1307,1308,1,0,0,0,1308,1309,6,161,17,0,1309,1310,6, - 161,11,0,1310,338,1,0,0,0,1311,1312,3,61,23,0,1312,1313,1,0,0,0,1313,1314, - 6,162,12,0,1314,340,1,0,0,0,1315,1321,3,75,30,0,1316,1321,3,65,25,0,1317, - 1321,3,105,45,0,1318,1321,3,67,26,0,1319,1321,3,81,33,0,1320,1315,1,0,0, - 0,1320,1316,1,0,0,0,1320,1317,1,0,0,0,1320,1318,1,0,0,0,1320,1319,1,0,0, - 0,1321,1322,1,0,0,0,1322,1320,1,0,0,0,1322,1323,1,0,0,0,1323,342,1,0,0, - 0,1324,1325,3,55,20,0,1325,1326,1,0,0,0,1326,1327,6,164,10,0,1327,344,1, - 0,0,0,1328,1329,3,57,21,0,1329,1330,1,0,0,0,1330,1331,6,165,10,0,1331,346, - 1,0,0,0,1332,1333,3,59,22,0,1333,1334,1,0,0,0,1334,1335,6,166,10,0,1335, - 348,1,0,0,0,1336,1337,3,63,24,0,1337,1338,1,0,0,0,1338,1339,6,167,16,0, - 1339,1340,6,167,11,0,1340,350,1,0,0,0,1341,1342,3,61,23,0,1342,1343,1,0, - 0,0,1343,1344,6,168,12,0,1344,352,1,0,0,0,1345,1346,3,101,43,0,1346,1347, - 1,0,0,0,1347,1348,6,169,18,0,1348,354,1,0,0,0,1349,1350,3,105,45,0,1350, - 1351,1,0,0,0,1351,1352,6,170,22,0,1352,356,1,0,0,0,1353,1354,3,269,127, - 0,1354,1355,1,0,0,0,1355,1356,6,171,32,0,1356,1357,6,171,33,0,1357,358, - 1,0,0,0,1358,1359,3,209,97,0,1359,1360,1,0,0,0,1360,1361,6,172,20,0,1361, - 360,1,0,0,0,1362,1363,3,85,35,0,1363,1364,1,0,0,0,1364,1365,6,173,21,0, - 1365,362,1,0,0,0,1366,1367,3,55,20,0,1367,1368,1,0,0,0,1368,1369,6,174, - 10,0,1369,364,1,0,0,0,1370,1371,3,57,21,0,1371,1372,1,0,0,0,1372,1373,6, - 175,10,0,1373,366,1,0,0,0,1374,1375,3,59,22,0,1375,1376,1,0,0,0,1376,1377, - 6,176,10,0,1377,368,1,0,0,0,1378,1379,3,63,24,0,1379,1380,1,0,0,0,1380, - 1381,6,177,16,0,1381,1382,6,177,11,0,1382,1383,6,177,11,0,1383,370,1,0, - 0,0,1384,1385,3,101,43,0,1385,1386,1,0,0,0,1386,1387,6,178,18,0,1387,372, - 1,0,0,0,1388,1389,3,105,45,0,1389,1390,1,0,0,0,1390,1391,6,179,22,0,1391, - 374,1,0,0,0,1392,1393,3,235,110,0,1393,1394,1,0,0,0,1394,1395,6,180,25, - 0,1395,376,1,0,0,0,1396,1397,3,55,20,0,1397,1398,1,0,0,0,1398,1399,6,181, - 10,0,1399,378,1,0,0,0,1400,1401,3,57,21,0,1401,1402,1,0,0,0,1402,1403,6, - 182,10,0,1403,380,1,0,0,0,1404,1405,3,59,22,0,1405,1406,1,0,0,0,1406,1407, - 6,183,10,0,1407,382,1,0,0,0,1408,1409,3,63,24,0,1409,1410,1,0,0,0,1410, - 1411,6,184,16,0,1411,1412,6,184,11,0,1412,384,1,0,0,0,1413,1414,3,209,97, - 0,1414,1415,1,0,0,0,1415,1416,6,185,20,0,1416,1417,6,185,11,0,1417,1418, - 6,185,34,0,1418,386,1,0,0,0,1419,1420,3,85,35,0,1420,1421,1,0,0,0,1421, - 1422,6,186,21,0,1422,1423,6,186,11,0,1423,1424,6,186,34,0,1424,388,1,0, - 0,0,1425,1426,3,55,20,0,1426,1427,1,0,0,0,1427,1428,6,187,10,0,1428,390, - 1,0,0,0,1429,1430,3,57,21,0,1430,1431,1,0,0,0,1431,1432,6,188,10,0,1432, - 392,1,0,0,0,1433,1434,3,59,22,0,1434,1435,1,0,0,0,1435,1436,6,189,10,0, - 1436,394,1,0,0,0,1437,1438,3,61,23,0,1438,1439,1,0,0,0,1439,1440,6,190, - 12,0,1440,1441,6,190,11,0,1441,1442,6,190,9,0,1442,396,1,0,0,0,1443,1444, - 3,101,43,0,1444,1445,1,0,0,0,1445,1446,6,191,18,0,1446,1447,6,191,11,0, - 1447,1448,6,191,9,0,1448,398,1,0,0,0,1449,1450,3,55,20,0,1450,1451,1,0, - 0,0,1451,1452,6,192,10,0,1452,400,1,0,0,0,1453,1454,3,57,21,0,1454,1455, - 1,0,0,0,1455,1456,6,193,10,0,1456,402,1,0,0,0,1457,1458,3,59,22,0,1458, - 1459,1,0,0,0,1459,1460,6,194,10,0,1460,404,1,0,0,0,1461,1462,3,175,80,0, - 1462,1463,1,0,0,0,1463,1464,6,195,11,0,1464,1465,6,195,0,0,1465,1466,6, - 195,30,0,1466,406,1,0,0,0,1467,1468,3,171,78,0,1468,1469,1,0,0,0,1469,1470, - 6,196,11,0,1470,1471,6,196,0,0,1471,1472,6,196,31,0,1472,408,1,0,0,0,1473, - 1474,3,91,38,0,1474,1475,1,0,0,0,1475,1476,6,197,11,0,1476,1477,6,197,0, - 0,1477,1478,6,197,35,0,1478,410,1,0,0,0,1479,1480,3,63,24,0,1480,1481,1, - 0,0,0,1481,1482,6,198,16,0,1482,1483,6,198,11,0,1483,412,1,0,0,0,65,0,1, - 2,3,4,5,6,7,8,9,10,11,12,13,14,581,591,595,598,607,609,620,641,646,655, - 662,667,669,680,688,691,693,698,703,709,716,721,727,730,738,742,873,878, - 885,887,903,908,913,915,921,998,1003,1052,1056,1061,1066,1071,1073,1077, - 1079,1166,1170,1175,1320,1322,36,5,1,0,5,4,0,5,6,0,5,2,0,5,3,0,5,8,0,5, - 5,0,5,9,0,5,11,0,5,13,0,0,1,0,4,0,0,7,24,0,7,16,0,7,65,0,5,0,0,7,25,0,7, - 66,0,7,34,0,7,32,0,7,76,0,7,26,0,7,36,0,7,48,0,7,64,0,7,80,0,5,10,0,5,7, - 0,7,90,0,7,89,0,7,68,0,7,67,0,7,88,0,5,12,0,5,14,0,7,29,0]; + 2,199,7,199,2,200,7,200,2,201,7,201,2,202,7,202,2,203,7,203,2,204,7,204, + 2,205,7,205,2,206,7,206,2,207,7,207,2,208,7,208,2,209,7,209,2,210,7,210, + 2,211,7,211,2,212,7,212,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3, + 1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5, + 1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8, + 1,8,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9, + 1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1, + 11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13, + 1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1, + 15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16, + 1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1, + 18,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,19,1,19,1,19,1,19, + 1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,21,1,21,1, + 21,1,21,1,21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22, + 1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,4,24,654,8,24,11, + 24,12,24,655,1,24,1,24,1,25,1,25,1,25,1,25,5,25,664,8,25,10,25,12,25,667, + 9,25,1,25,3,25,670,8,25,1,25,3,25,673,8,25,1,25,1,25,1,26,1,26,1,26,1,26, + 1,26,5,26,682,8,26,10,26,12,26,685,9,26,1,26,1,26,1,26,1,26,1,26,1,27,4, + 27,693,8,27,11,27,12,27,694,1,27,1,27,1,28,1,28,1,28,1,28,1,29,1,29,1,30, + 1,30,1,31,1,31,1,31,1,32,1,32,1,33,1,33,3,33,714,8,33,1,33,4,33,717,8,33, + 11,33,12,33,718,1,34,1,34,1,35,1,35,1,36,1,36,1,36,3,36,728,8,36,1,37,1, + 37,1,38,1,38,1,38,3,38,735,8,38,1,39,1,39,1,39,5,39,740,8,39,10,39,12,39, + 743,9,39,1,39,1,39,1,39,1,39,1,39,1,39,5,39,751,8,39,10,39,12,39,754,9, + 39,1,39,1,39,1,39,1,39,1,39,3,39,761,8,39,1,39,3,39,764,8,39,3,39,766,8, + 39,1,40,4,40,769,8,40,11,40,12,40,770,1,41,4,41,774,8,41,11,41,12,41,775, + 1,41,1,41,5,41,780,8,41,10,41,12,41,783,9,41,1,41,1,41,4,41,787,8,41,11, + 41,12,41,788,1,41,4,41,792,8,41,11,41,12,41,793,1,41,1,41,5,41,798,8,41, + 10,41,12,41,801,9,41,3,41,803,8,41,1,41,1,41,1,41,1,41,4,41,809,8,41,11, + 41,12,41,810,1,41,1,41,3,41,815,8,41,1,42,1,42,1,42,1,43,1,43,1,43,1,43, + 1,44,1,44,1,44,1,44,1,45,1,45,1,46,1,46,1,46,1,47,1,47,1,48,1,48,1,49,1, + 49,1,49,1,49,1,49,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,1,52,1,52,1,52, + 1,52,1,52,1,52,1,53,1,53,1,53,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,1, + 56,1,56,1,56,1,56,1,56,1,57,1,57,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59, + 1,59,1,60,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,62,1,62,1,63,1,63,1, + 63,1,63,1,63,1,63,1,64,1,64,1,65,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,67, + 1,67,1,67,1,68,1,68,1,68,1,69,1,69,1,70,1,70,1,70,1,71,1,71,1,72,1,72,1, + 72,1,73,1,73,1,74,1,74,1,75,1,75,1,76,1,76,1,77,1,77,1,78,1,78,1,78,1,78, + 1,79,1,79,1,79,3,79,943,8,79,1,79,5,79,946,8,79,10,79,12,79,949,9,79,1, + 79,1,79,4,79,953,8,79,11,79,12,79,954,3,79,957,8,79,1,80,1,80,1,80,1,80, + 1,80,1,81,1,81,1,81,1,81,1,81,1,82,1,82,5,82,971,8,82,10,82,12,82,974,9, + 82,1,82,1,82,3,82,978,8,82,1,82,4,82,981,8,82,11,82,12,82,982,3,82,985, + 8,82,1,83,1,83,4,83,989,8,83,11,83,12,83,990,1,83,1,83,1,84,1,84,1,85,1, + 85,1,85,1,85,1,86,1,86,1,86,1,86,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88, + 1,88,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,90,1,91,1,91,1,91,1,91,1, + 92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1,95,1,95, + 1,95,1,95,1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,98,1,98,1,98,1,98,1, + 99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,3,100,1068, + 8,100,1,101,4,101,1071,8,101,11,101,12,101,1072,1,102,1,102,1,102,1,102, + 1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,105,1,105,1,105,1,105, + 1,106,1,106,1,106,1,106,1,107,1,107,1,107,1,107,1,107,1,108,1,108,1,108, + 1,108,1,109,1,109,1,109,1,109,1,110,1,110,1,110,1,110,1,110,1,111,1,111, + 1,111,1,111,1,111,1,112,1,112,1,112,1,112,3,112,1122,8,112,1,113,1,113, + 3,113,1126,8,113,1,113,5,113,1129,8,113,10,113,12,113,1132,9,113,1,113, + 1,113,3,113,1136,8,113,1,113,4,113,1139,8,113,11,113,12,113,1140,3,113, + 1143,8,113,1,114,1,114,4,114,1147,8,114,11,114,12,114,1148,1,115,1,115, + 1,115,1,115,1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,118,1,118, + 1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,120,1,120,1,120,1,120,1,121, + 1,121,1,121,1,121,1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,123,1,123, + 1,123,1,124,1,124,1,124,1,125,1,125,1,125,1,125,1,126,1,126,1,126,1,126, + 1,127,1,127,1,127,1,127,1,128,1,128,1,128,1,128,1,129,1,129,1,129,1,129, + 1,129,1,130,1,130,1,130,1,130,1,130,1,131,1,131,1,131,1,131,1,131,1,132, + 1,132,1,132,1,132,1,132,1,132,1,132,1,133,1,133,1,134,4,134,1234,8,134, + 11,134,12,134,1235,1,134,1,134,3,134,1240,8,134,1,134,4,134,1243,8,134, + 11,134,12,134,1244,1,135,1,135,1,135,1,135,1,136,1,136,1,136,1,136,1,137, + 1,137,1,137,1,137,1,138,1,138,1,138,1,138,1,139,1,139,1,139,1,139,1,139, + 1,139,1,140,1,140,1,140,1,140,1,141,1,141,1,141,1,141,1,142,1,142,1,142, + 1,142,1,143,1,143,1,143,1,143,1,144,1,144,1,144,1,144,1,145,1,145,1,145, + 1,145,1,146,1,146,1,146,1,146,1,146,1,147,1,147,1,147,1,147,1,147,1,148, + 1,148,1,148,1,148,1,149,1,149,1,149,1,149,1,150,1,150,1,150,1,150,1,151, + 1,151,1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,153,1,153,1,153,1,153, + 1,153,1,154,1,154,1,154,1,154,1,154,1,155,1,155,1,155,1,155,1,156,1,156, + 1,156,1,156,1,157,1,157,1,157,1,157,1,158,1,158,1,158,1,158,1,159,1,159, + 1,159,1,159,1,160,1,160,1,160,1,160,1,160,1,161,1,161,1,161,1,161,1,161, + 1,162,1,162,1,162,1,162,1,163,1,163,1,163,1,163,1,164,1,164,1,164,1,164, + 1,165,1,165,1,165,1,165,1,165,1,166,1,166,1,166,1,166,1,167,1,167,1,167, + 1,167,1,167,4,167,1390,8,167,11,167,12,167,1391,1,168,1,168,1,168,1,168, + 1,169,1,169,1,169,1,169,1,170,1,170,1,170,1,170,1,171,1,171,1,171,1,171, + 1,171,1,172,1,172,1,172,1,172,1,173,1,173,1,173,1,173,1,174,1,174,1,174, + 1,174,1,175,1,175,1,175,1,175,1,175,1,176,1,176,1,176,1,176,1,177,1,177, + 1,177,1,177,1,178,1,178,1,178,1,178,1,179,1,179,1,179,1,179,1,180,1,180, + 1,180,1,180,1,181,1,181,1,181,1,181,1,181,1,181,1,182,1,182,1,182,1,182, + 1,183,1,183,1,183,1,183,1,184,1,184,1,184,1,184,1,185,1,185,1,185,1,185, + 1,186,1,186,1,186,1,186,1,187,1,187,1,187,1,187,1,188,1,188,1,188,1,188, + 1,188,1,189,1,189,1,189,1,189,1,190,1,190,1,190,1,190,1,191,1,191,1,191, + 1,191,1,191,1,191,1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192, + 1,193,1,193,1,193,1,193,1,194,1,194,1,194,1,194,1,195,1,195,1,195,1,195, + 1,196,1,196,1,196,1,196,1,197,1,197,1,197,1,197,1,198,1,198,1,198,1,198, + 1,198,1,199,1,199,1,199,1,199,1,199,1,199,1,200,1,200,1,200,1,200,1,200, + 1,200,1,201,1,201,1,201,1,201,1,202,1,202,1,202,1,202,1,203,1,203,1,203, + 1,203,1,204,1,204,1,204,1,204,1,204,1,204,1,205,1,205,1,205,1,205,1,205, + 1,205,1,206,1,206,1,206,1,206,1,207,1,207,1,207,1,207,1,208,1,208,1,208, + 1,208,1,209,1,209,1,209,1,209,1,209,1,209,1,210,1,210,1,210,1,210,1,210, + 1,210,1,211,1,211,1,211,1,211,1,211,1,211,1,212,1,212,1,212,1,212,1,212, + 2,683,752,0,213,16,1,18,2,20,3,22,4,24,5,26,6,28,7,30,8,32,9,34,10,36,11, + 38,12,40,13,42,14,44,15,46,16,48,17,50,18,52,19,54,20,56,21,58,22,60,23, + 62,24,64,25,66,26,68,27,70,28,72,29,74,0,76,0,78,0,80,0,82,0,84,0,86,0, + 88,0,90,0,92,0,94,30,96,31,98,32,100,33,102,34,104,35,106,36,108,37,110, + 38,112,39,114,40,116,41,118,42,120,43,122,44,124,45,126,46,128,47,130,48, + 132,49,134,50,136,51,138,52,140,53,142,54,144,55,146,56,148,57,150,58,152, + 59,154,60,156,61,158,62,160,63,162,64,164,65,166,66,168,67,170,68,172,0, + 174,69,176,70,178,71,180,72,182,0,184,73,186,74,188,75,190,76,192,0,194, + 0,196,77,198,78,200,79,202,0,204,0,206,0,208,0,210,0,212,0,214,80,216,0, + 218,81,220,0,222,0,224,82,226,83,228,84,230,0,232,0,234,0,236,0,238,0,240, + 0,242,0,244,85,246,86,248,87,250,88,252,0,254,0,256,0,258,0,260,0,262,0, + 264,89,266,0,268,90,270,91,272,92,274,0,276,0,278,93,280,94,282,0,284,95, + 286,0,288,96,290,97,292,98,294,0,296,0,298,0,300,0,302,0,304,0,306,0,308, + 0,310,0,312,99,314,100,316,101,318,0,320,0,322,0,324,0,326,0,328,0,330, + 102,332,103,334,104,336,0,338,105,340,106,342,107,344,108,346,0,348,0,350, + 109,352,110,354,111,356,112,358,0,360,0,362,0,364,0,366,0,368,0,370,0,372, + 113,374,114,376,115,378,0,380,0,382,0,384,0,386,116,388,117,390,118,392, + 0,394,0,396,0,398,0,400,119,402,0,404,0,406,120,408,121,410,122,412,0,414, + 0,416,0,418,123,420,124,422,125,424,0,426,0,428,126,430,127,432,128,434, + 0,436,0,438,0,440,0,16,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,36,2,0,68, + 68,100,100,2,0,73,73,105,105,2,0,83,83,115,115,2,0,69,69,101,101,2,0,67, + 67,99,99,2,0,84,84,116,116,2,0,82,82,114,114,2,0,79,79,111,111,2,0,80,80, + 112,112,2,0,78,78,110,110,2,0,72,72,104,104,2,0,86,86,118,118,2,0,65,65, + 97,97,2,0,76,76,108,108,2,0,88,88,120,120,2,0,70,70,102,102,2,0,77,77,109, + 109,2,0,71,71,103,103,2,0,75,75,107,107,2,0,87,87,119,119,2,0,85,85,117, + 117,2,0,74,74,106,106,6,0,9,10,13,13,32,32,47,47,91,91,93,93,2,0,10,10, + 13,13,3,0,9,10,13,13,32,32,1,0,48,57,2,0,65,90,97,122,8,0,34,34,78,78,82, + 82,84,84,92,92,110,110,114,114,116,116,4,0,10,10,13,13,34,34,92,92,2,0, + 43,43,45,45,1,0,96,96,2,0,66,66,98,98,2,0,89,89,121,121,11,0,9,10,13,13, + 32,32,34,34,44,44,47,47,58,58,61,61,91,91,93,93,124,124,2,0,42,42,47,47, + 11,0,9,10,13,13,32,32,34,35,44,44,47,47,58,58,60,60,62,63,92,92,124,124, + 1628,0,16,1,0,0,0,0,18,1,0,0,0,0,20,1,0,0,0,0,22,1,0,0,0,0,24,1,0,0,0,0, + 26,1,0,0,0,0,28,1,0,0,0,0,30,1,0,0,0,0,32,1,0,0,0,0,34,1,0,0,0,0,36,1,0, + 0,0,0,38,1,0,0,0,0,40,1,0,0,0,0,42,1,0,0,0,0,44,1,0,0,0,0,46,1,0,0,0,0, + 48,1,0,0,0,0,50,1,0,0,0,0,52,1,0,0,0,0,54,1,0,0,0,0,56,1,0,0,0,0,58,1,0, + 0,0,0,60,1,0,0,0,0,62,1,0,0,0,0,64,1,0,0,0,0,66,1,0,0,0,0,68,1,0,0,0,0, + 70,1,0,0,0,1,72,1,0,0,0,1,94,1,0,0,0,1,96,1,0,0,0,1,98,1,0,0,0,1,100,1, + 0,0,0,1,102,1,0,0,0,1,104,1,0,0,0,1,106,1,0,0,0,1,108,1,0,0,0,1,110,1,0, + 0,0,1,112,1,0,0,0,1,114,1,0,0,0,1,116,1,0,0,0,1,118,1,0,0,0,1,120,1,0,0, + 0,1,122,1,0,0,0,1,124,1,0,0,0,1,126,1,0,0,0,1,128,1,0,0,0,1,130,1,0,0,0, + 1,132,1,0,0,0,1,134,1,0,0,0,1,136,1,0,0,0,1,138,1,0,0,0,1,140,1,0,0,0,1, + 142,1,0,0,0,1,144,1,0,0,0,1,146,1,0,0,0,1,148,1,0,0,0,1,150,1,0,0,0,1,152, + 1,0,0,0,1,154,1,0,0,0,1,156,1,0,0,0,1,158,1,0,0,0,1,160,1,0,0,0,1,162,1, + 0,0,0,1,164,1,0,0,0,1,166,1,0,0,0,1,168,1,0,0,0,1,170,1,0,0,0,1,172,1,0, + 0,0,1,174,1,0,0,0,1,176,1,0,0,0,1,178,1,0,0,0,1,180,1,0,0,0,1,184,1,0,0, + 0,1,186,1,0,0,0,1,188,1,0,0,0,1,190,1,0,0,0,2,192,1,0,0,0,2,194,1,0,0,0, + 2,196,1,0,0,0,2,198,1,0,0,0,2,200,1,0,0,0,3,202,1,0,0,0,3,204,1,0,0,0,3, + 206,1,0,0,0,3,208,1,0,0,0,3,210,1,0,0,0,3,212,1,0,0,0,3,214,1,0,0,0,3,218, + 1,0,0,0,3,220,1,0,0,0,3,222,1,0,0,0,3,224,1,0,0,0,3,226,1,0,0,0,3,228,1, + 0,0,0,4,230,1,0,0,0,4,232,1,0,0,0,4,234,1,0,0,0,4,236,1,0,0,0,4,238,1,0, + 0,0,4,244,1,0,0,0,4,246,1,0,0,0,4,248,1,0,0,0,4,250,1,0,0,0,5,252,1,0,0, + 0,5,254,1,0,0,0,5,256,1,0,0,0,5,258,1,0,0,0,5,260,1,0,0,0,5,262,1,0,0,0, + 5,264,1,0,0,0,5,266,1,0,0,0,5,268,1,0,0,0,5,270,1,0,0,0,5,272,1,0,0,0,6, + 274,1,0,0,0,6,276,1,0,0,0,6,278,1,0,0,0,6,280,1,0,0,0,6,284,1,0,0,0,6,286, + 1,0,0,0,6,288,1,0,0,0,6,290,1,0,0,0,6,292,1,0,0,0,7,294,1,0,0,0,7,296,1, + 0,0,0,7,298,1,0,0,0,7,300,1,0,0,0,7,302,1,0,0,0,7,304,1,0,0,0,7,306,1,0, + 0,0,7,308,1,0,0,0,7,310,1,0,0,0,7,312,1,0,0,0,7,314,1,0,0,0,7,316,1,0,0, + 0,8,318,1,0,0,0,8,320,1,0,0,0,8,322,1,0,0,0,8,324,1,0,0,0,8,326,1,0,0,0, + 8,328,1,0,0,0,8,330,1,0,0,0,8,332,1,0,0,0,8,334,1,0,0,0,9,336,1,0,0,0,9, + 338,1,0,0,0,9,340,1,0,0,0,9,342,1,0,0,0,9,344,1,0,0,0,10,346,1,0,0,0,10, + 348,1,0,0,0,10,350,1,0,0,0,10,352,1,0,0,0,10,354,1,0,0,0,10,356,1,0,0,0, + 11,358,1,0,0,0,11,360,1,0,0,0,11,362,1,0,0,0,11,364,1,0,0,0,11,366,1,0, + 0,0,11,368,1,0,0,0,11,370,1,0,0,0,11,372,1,0,0,0,11,374,1,0,0,0,11,376, + 1,0,0,0,12,378,1,0,0,0,12,380,1,0,0,0,12,382,1,0,0,0,12,384,1,0,0,0,12, + 386,1,0,0,0,12,388,1,0,0,0,12,390,1,0,0,0,13,392,1,0,0,0,13,394,1,0,0,0, + 13,396,1,0,0,0,13,398,1,0,0,0,13,400,1,0,0,0,13,402,1,0,0,0,13,404,1,0, + 0,0,13,406,1,0,0,0,13,408,1,0,0,0,13,410,1,0,0,0,14,412,1,0,0,0,14,414, + 1,0,0,0,14,416,1,0,0,0,14,418,1,0,0,0,14,420,1,0,0,0,14,422,1,0,0,0,15, + 424,1,0,0,0,15,426,1,0,0,0,15,428,1,0,0,0,15,430,1,0,0,0,15,432,1,0,0,0, + 15,434,1,0,0,0,15,436,1,0,0,0,15,438,1,0,0,0,15,440,1,0,0,0,16,442,1,0, + 0,0,18,452,1,0,0,0,20,459,1,0,0,0,22,468,1,0,0,0,24,475,1,0,0,0,26,485, + 1,0,0,0,28,492,1,0,0,0,30,499,1,0,0,0,32,506,1,0,0,0,34,514,1,0,0,0,36, + 526,1,0,0,0,38,535,1,0,0,0,40,541,1,0,0,0,42,548,1,0,0,0,44,555,1,0,0,0, + 46,563,1,0,0,0,48,571,1,0,0,0,50,586,1,0,0,0,52,598,1,0,0,0,54,609,1,0, + 0,0,56,617,1,0,0,0,58,625,1,0,0,0,60,633,1,0,0,0,62,642,1,0,0,0,64,653, + 1,0,0,0,66,659,1,0,0,0,68,676,1,0,0,0,70,692,1,0,0,0,72,698,1,0,0,0,74, + 702,1,0,0,0,76,704,1,0,0,0,78,706,1,0,0,0,80,709,1,0,0,0,82,711,1,0,0,0, + 84,720,1,0,0,0,86,722,1,0,0,0,88,727,1,0,0,0,90,729,1,0,0,0,92,734,1,0, + 0,0,94,765,1,0,0,0,96,768,1,0,0,0,98,814,1,0,0,0,100,816,1,0,0,0,102,819, + 1,0,0,0,104,823,1,0,0,0,106,827,1,0,0,0,108,829,1,0,0,0,110,832,1,0,0,0, + 112,834,1,0,0,0,114,836,1,0,0,0,116,841,1,0,0,0,118,843,1,0,0,0,120,849, + 1,0,0,0,122,855,1,0,0,0,124,858,1,0,0,0,126,861,1,0,0,0,128,866,1,0,0,0, + 130,871,1,0,0,0,132,873,1,0,0,0,134,877,1,0,0,0,136,882,1,0,0,0,138,888, + 1,0,0,0,140,891,1,0,0,0,142,893,1,0,0,0,144,899,1,0,0,0,146,901,1,0,0,0, + 148,906,1,0,0,0,150,909,1,0,0,0,152,912,1,0,0,0,154,915,1,0,0,0,156,917, + 1,0,0,0,158,920,1,0,0,0,160,922,1,0,0,0,162,925,1,0,0,0,164,927,1,0,0,0, + 166,929,1,0,0,0,168,931,1,0,0,0,170,933,1,0,0,0,172,935,1,0,0,0,174,956, + 1,0,0,0,176,958,1,0,0,0,178,963,1,0,0,0,180,984,1,0,0,0,182,986,1,0,0,0, + 184,994,1,0,0,0,186,996,1,0,0,0,188,1000,1,0,0,0,190,1004,1,0,0,0,192,1008, + 1,0,0,0,194,1013,1,0,0,0,196,1018,1,0,0,0,198,1022,1,0,0,0,200,1026,1,0, + 0,0,202,1030,1,0,0,0,204,1035,1,0,0,0,206,1039,1,0,0,0,208,1043,1,0,0,0, + 210,1047,1,0,0,0,212,1051,1,0,0,0,214,1055,1,0,0,0,216,1067,1,0,0,0,218, + 1070,1,0,0,0,220,1074,1,0,0,0,222,1078,1,0,0,0,224,1082,1,0,0,0,226,1086, + 1,0,0,0,228,1090,1,0,0,0,230,1094,1,0,0,0,232,1099,1,0,0,0,234,1103,1,0, + 0,0,236,1107,1,0,0,0,238,1112,1,0,0,0,240,1121,1,0,0,0,242,1142,1,0,0,0, + 244,1146,1,0,0,0,246,1150,1,0,0,0,248,1154,1,0,0,0,250,1158,1,0,0,0,252, + 1162,1,0,0,0,254,1167,1,0,0,0,256,1171,1,0,0,0,258,1175,1,0,0,0,260,1179, + 1,0,0,0,262,1184,1,0,0,0,264,1189,1,0,0,0,266,1192,1,0,0,0,268,1196,1,0, + 0,0,270,1200,1,0,0,0,272,1204,1,0,0,0,274,1208,1,0,0,0,276,1213,1,0,0,0, + 278,1218,1,0,0,0,280,1223,1,0,0,0,282,1230,1,0,0,0,284,1239,1,0,0,0,286, + 1246,1,0,0,0,288,1250,1,0,0,0,290,1254,1,0,0,0,292,1258,1,0,0,0,294,1262, + 1,0,0,0,296,1268,1,0,0,0,298,1272,1,0,0,0,300,1276,1,0,0,0,302,1280,1,0, + 0,0,304,1284,1,0,0,0,306,1288,1,0,0,0,308,1292,1,0,0,0,310,1297,1,0,0,0, + 312,1302,1,0,0,0,314,1306,1,0,0,0,316,1310,1,0,0,0,318,1314,1,0,0,0,320, + 1319,1,0,0,0,322,1323,1,0,0,0,324,1328,1,0,0,0,326,1333,1,0,0,0,328,1337, + 1,0,0,0,330,1341,1,0,0,0,332,1345,1,0,0,0,334,1349,1,0,0,0,336,1353,1,0, + 0,0,338,1358,1,0,0,0,340,1363,1,0,0,0,342,1367,1,0,0,0,344,1371,1,0,0,0, + 346,1375,1,0,0,0,348,1380,1,0,0,0,350,1389,1,0,0,0,352,1393,1,0,0,0,354, + 1397,1,0,0,0,356,1401,1,0,0,0,358,1405,1,0,0,0,360,1410,1,0,0,0,362,1414, + 1,0,0,0,364,1418,1,0,0,0,366,1422,1,0,0,0,368,1427,1,0,0,0,370,1431,1,0, + 0,0,372,1435,1,0,0,0,374,1439,1,0,0,0,376,1443,1,0,0,0,378,1447,1,0,0,0, + 380,1453,1,0,0,0,382,1457,1,0,0,0,384,1461,1,0,0,0,386,1465,1,0,0,0,388, + 1469,1,0,0,0,390,1473,1,0,0,0,392,1477,1,0,0,0,394,1482,1,0,0,0,396,1486, + 1,0,0,0,398,1490,1,0,0,0,400,1496,1,0,0,0,402,1505,1,0,0,0,404,1509,1,0, + 0,0,406,1513,1,0,0,0,408,1517,1,0,0,0,410,1521,1,0,0,0,412,1525,1,0,0,0, + 414,1530,1,0,0,0,416,1536,1,0,0,0,418,1542,1,0,0,0,420,1546,1,0,0,0,422, + 1550,1,0,0,0,424,1554,1,0,0,0,426,1560,1,0,0,0,428,1566,1,0,0,0,430,1570, + 1,0,0,0,432,1574,1,0,0,0,434,1578,1,0,0,0,436,1584,1,0,0,0,438,1590,1,0, + 0,0,440,1596,1,0,0,0,442,443,7,0,0,0,443,444,7,1,0,0,444,445,7,2,0,0,445, + 446,7,2,0,0,446,447,7,3,0,0,447,448,7,4,0,0,448,449,7,5,0,0,449,450,1,0, + 0,0,450,451,6,0,0,0,451,17,1,0,0,0,452,453,7,0,0,0,453,454,7,6,0,0,454, + 455,7,7,0,0,455,456,7,8,0,0,456,457,1,0,0,0,457,458,6,1,1,0,458,19,1,0, + 0,0,459,460,7,3,0,0,460,461,7,9,0,0,461,462,7,6,0,0,462,463,7,1,0,0,463, + 464,7,4,0,0,464,465,7,10,0,0,465,466,1,0,0,0,466,467,6,2,2,0,467,21,1,0, + 0,0,468,469,7,3,0,0,469,470,7,11,0,0,470,471,7,12,0,0,471,472,7,13,0,0, + 472,473,1,0,0,0,473,474,6,3,0,0,474,23,1,0,0,0,475,476,7,3,0,0,476,477, + 7,14,0,0,477,478,7,8,0,0,478,479,7,13,0,0,479,480,7,12,0,0,480,481,7,1, + 0,0,481,482,7,9,0,0,482,483,1,0,0,0,483,484,6,4,3,0,484,25,1,0,0,0,485, + 486,7,15,0,0,486,487,7,6,0,0,487,488,7,7,0,0,488,489,7,16,0,0,489,490,1, + 0,0,0,490,491,6,5,4,0,491,27,1,0,0,0,492,493,7,17,0,0,493,494,7,6,0,0,494, + 495,7,7,0,0,495,496,7,18,0,0,496,497,1,0,0,0,497,498,6,6,0,0,498,29,1,0, + 0,0,499,500,7,18,0,0,500,501,7,3,0,0,501,502,7,3,0,0,502,503,7,8,0,0,503, + 504,1,0,0,0,504,505,6,7,1,0,505,31,1,0,0,0,506,507,7,13,0,0,507,508,7,1, + 0,0,508,509,7,16,0,0,509,510,7,1,0,0,510,511,7,5,0,0,511,512,1,0,0,0,512, + 513,6,8,0,0,513,33,1,0,0,0,514,515,7,16,0,0,515,516,7,11,0,0,516,517,5, + 95,0,0,517,518,7,3,0,0,518,519,7,14,0,0,519,520,7,8,0,0,520,521,7,12,0, + 0,521,522,7,9,0,0,522,523,7,0,0,0,523,524,1,0,0,0,524,525,6,9,5,0,525,35, + 1,0,0,0,526,527,7,6,0,0,527,528,7,3,0,0,528,529,7,9,0,0,529,530,7,12,0, + 0,530,531,7,16,0,0,531,532,7,3,0,0,532,533,1,0,0,0,533,534,6,10,6,0,534, + 37,1,0,0,0,535,536,7,6,0,0,536,537,7,7,0,0,537,538,7,19,0,0,538,539,1,0, + 0,0,539,540,6,11,0,0,540,39,1,0,0,0,541,542,7,2,0,0,542,543,7,10,0,0,543, + 544,7,7,0,0,544,545,7,19,0,0,545,546,1,0,0,0,546,547,6,12,7,0,547,41,1, + 0,0,0,548,549,7,2,0,0,549,550,7,7,0,0,550,551,7,6,0,0,551,552,7,5,0,0,552, + 553,1,0,0,0,553,554,6,13,0,0,554,43,1,0,0,0,555,556,7,2,0,0,556,557,7,5, + 0,0,557,558,7,12,0,0,558,559,7,5,0,0,559,560,7,2,0,0,560,561,1,0,0,0,561, + 562,6,14,0,0,562,45,1,0,0,0,563,564,7,19,0,0,564,565,7,10,0,0,565,566,7, + 3,0,0,566,567,7,6,0,0,567,568,7,3,0,0,568,569,1,0,0,0,569,570,6,15,0,0, + 570,47,1,0,0,0,571,572,4,16,0,0,572,573,7,1,0,0,573,574,7,9,0,0,574,575, + 7,13,0,0,575,576,7,1,0,0,576,577,7,9,0,0,577,578,7,3,0,0,578,579,7,2,0, + 0,579,580,7,5,0,0,580,581,7,12,0,0,581,582,7,5,0,0,582,583,7,2,0,0,583, + 584,1,0,0,0,584,585,6,16,0,0,585,49,1,0,0,0,586,587,4,17,1,0,587,588,7, + 13,0,0,588,589,7,7,0,0,589,590,7,7,0,0,590,591,7,18,0,0,591,592,7,20,0, + 0,592,593,7,8,0,0,593,594,5,95,0,0,594,595,5,128020,0,0,595,596,1,0,0,0, + 596,597,6,17,8,0,597,51,1,0,0,0,598,599,4,18,2,0,599,600,7,16,0,0,600,601, + 7,3,0,0,601,602,7,5,0,0,602,603,7,6,0,0,603,604,7,1,0,0,604,605,7,4,0,0, + 605,606,7,2,0,0,606,607,1,0,0,0,607,608,6,18,9,0,608,53,1,0,0,0,609,610, + 4,19,3,0,610,611,7,21,0,0,611,612,7,7,0,0,612,613,7,1,0,0,613,614,7,9,0, + 0,614,615,1,0,0,0,615,616,6,19,10,0,616,55,1,0,0,0,617,618,4,20,4,0,618, + 619,7,15,0,0,619,620,7,20,0,0,620,621,7,13,0,0,621,622,7,13,0,0,622,623, + 1,0,0,0,623,624,6,20,10,0,624,57,1,0,0,0,625,626,4,21,5,0,626,627,7,13, + 0,0,627,628,7,3,0,0,628,629,7,15,0,0,629,630,7,5,0,0,630,631,1,0,0,0,631, + 632,6,21,10,0,632,59,1,0,0,0,633,634,4,22,6,0,634,635,7,6,0,0,635,636,7, + 1,0,0,636,637,7,17,0,0,637,638,7,10,0,0,638,639,7,5,0,0,639,640,1,0,0,0, + 640,641,6,22,10,0,641,61,1,0,0,0,642,643,4,23,7,0,643,644,7,13,0,0,644, + 645,7,7,0,0,645,646,7,7,0,0,646,647,7,18,0,0,647,648,7,20,0,0,648,649,7, + 8,0,0,649,650,1,0,0,0,650,651,6,23,10,0,651,63,1,0,0,0,652,654,8,22,0,0, + 653,652,1,0,0,0,654,655,1,0,0,0,655,653,1,0,0,0,655,656,1,0,0,0,656,657, + 1,0,0,0,657,658,6,24,0,0,658,65,1,0,0,0,659,660,5,47,0,0,660,661,5,47,0, + 0,661,665,1,0,0,0,662,664,8,23,0,0,663,662,1,0,0,0,664,667,1,0,0,0,665, + 663,1,0,0,0,665,666,1,0,0,0,666,669,1,0,0,0,667,665,1,0,0,0,668,670,5,13, + 0,0,669,668,1,0,0,0,669,670,1,0,0,0,670,672,1,0,0,0,671,673,5,10,0,0,672, + 671,1,0,0,0,672,673,1,0,0,0,673,674,1,0,0,0,674,675,6,25,11,0,675,67,1, + 0,0,0,676,677,5,47,0,0,677,678,5,42,0,0,678,683,1,0,0,0,679,682,3,68,26, + 0,680,682,9,0,0,0,681,679,1,0,0,0,681,680,1,0,0,0,682,685,1,0,0,0,683,684, + 1,0,0,0,683,681,1,0,0,0,684,686,1,0,0,0,685,683,1,0,0,0,686,687,5,42,0, + 0,687,688,5,47,0,0,688,689,1,0,0,0,689,690,6,26,11,0,690,69,1,0,0,0,691, + 693,7,24,0,0,692,691,1,0,0,0,693,694,1,0,0,0,694,692,1,0,0,0,694,695,1, + 0,0,0,695,696,1,0,0,0,696,697,6,27,11,0,697,71,1,0,0,0,698,699,5,124,0, + 0,699,700,1,0,0,0,700,701,6,28,12,0,701,73,1,0,0,0,702,703,7,25,0,0,703, + 75,1,0,0,0,704,705,7,26,0,0,705,77,1,0,0,0,706,707,5,92,0,0,707,708,7,27, + 0,0,708,79,1,0,0,0,709,710,8,28,0,0,710,81,1,0,0,0,711,713,7,3,0,0,712, + 714,7,29,0,0,713,712,1,0,0,0,713,714,1,0,0,0,714,716,1,0,0,0,715,717,3, + 74,29,0,716,715,1,0,0,0,717,718,1,0,0,0,718,716,1,0,0,0,718,719,1,0,0,0, + 719,83,1,0,0,0,720,721,5,64,0,0,721,85,1,0,0,0,722,723,5,96,0,0,723,87, + 1,0,0,0,724,728,8,30,0,0,725,726,5,96,0,0,726,728,5,96,0,0,727,724,1,0, + 0,0,727,725,1,0,0,0,728,89,1,0,0,0,729,730,5,95,0,0,730,91,1,0,0,0,731, + 735,3,76,30,0,732,735,3,74,29,0,733,735,3,90,37,0,734,731,1,0,0,0,734,732, + 1,0,0,0,734,733,1,0,0,0,735,93,1,0,0,0,736,741,5,34,0,0,737,740,3,78,31, + 0,738,740,3,80,32,0,739,737,1,0,0,0,739,738,1,0,0,0,740,743,1,0,0,0,741, + 739,1,0,0,0,741,742,1,0,0,0,742,744,1,0,0,0,743,741,1,0,0,0,744,766,5,34, + 0,0,745,746,5,34,0,0,746,747,5,34,0,0,747,748,5,34,0,0,748,752,1,0,0,0, + 749,751,8,23,0,0,750,749,1,0,0,0,751,754,1,0,0,0,752,753,1,0,0,0,752,750, + 1,0,0,0,753,755,1,0,0,0,754,752,1,0,0,0,755,756,5,34,0,0,756,757,5,34,0, + 0,757,758,5,34,0,0,758,760,1,0,0,0,759,761,5,34,0,0,760,759,1,0,0,0,760, + 761,1,0,0,0,761,763,1,0,0,0,762,764,5,34,0,0,763,762,1,0,0,0,763,764,1, + 0,0,0,764,766,1,0,0,0,765,736,1,0,0,0,765,745,1,0,0,0,766,95,1,0,0,0,767, + 769,3,74,29,0,768,767,1,0,0,0,769,770,1,0,0,0,770,768,1,0,0,0,770,771,1, + 0,0,0,771,97,1,0,0,0,772,774,3,74,29,0,773,772,1,0,0,0,774,775,1,0,0,0, + 775,773,1,0,0,0,775,776,1,0,0,0,776,777,1,0,0,0,777,781,3,116,50,0,778, + 780,3,74,29,0,779,778,1,0,0,0,780,783,1,0,0,0,781,779,1,0,0,0,781,782,1, + 0,0,0,782,815,1,0,0,0,783,781,1,0,0,0,784,786,3,116,50,0,785,787,3,74,29, + 0,786,785,1,0,0,0,787,788,1,0,0,0,788,786,1,0,0,0,788,789,1,0,0,0,789,815, + 1,0,0,0,790,792,3,74,29,0,791,790,1,0,0,0,792,793,1,0,0,0,793,791,1,0,0, + 0,793,794,1,0,0,0,794,802,1,0,0,0,795,799,3,116,50,0,796,798,3,74,29,0, + 797,796,1,0,0,0,798,801,1,0,0,0,799,797,1,0,0,0,799,800,1,0,0,0,800,803, + 1,0,0,0,801,799,1,0,0,0,802,795,1,0,0,0,802,803,1,0,0,0,803,804,1,0,0,0, + 804,805,3,82,33,0,805,815,1,0,0,0,806,808,3,116,50,0,807,809,3,74,29,0, + 808,807,1,0,0,0,809,810,1,0,0,0,810,808,1,0,0,0,810,811,1,0,0,0,811,812, + 1,0,0,0,812,813,3,82,33,0,813,815,1,0,0,0,814,773,1,0,0,0,814,784,1,0,0, + 0,814,791,1,0,0,0,814,806,1,0,0,0,815,99,1,0,0,0,816,817,7,31,0,0,817,818, + 7,32,0,0,818,101,1,0,0,0,819,820,7,12,0,0,820,821,7,9,0,0,821,822,7,0,0, + 0,822,103,1,0,0,0,823,824,7,12,0,0,824,825,7,2,0,0,825,826,7,4,0,0,826, + 105,1,0,0,0,827,828,5,61,0,0,828,107,1,0,0,0,829,830,5,58,0,0,830,831,5, + 58,0,0,831,109,1,0,0,0,832,833,5,58,0,0,833,111,1,0,0,0,834,835,5,44,0, + 0,835,113,1,0,0,0,836,837,7,0,0,0,837,838,7,3,0,0,838,839,7,2,0,0,839,840, + 7,4,0,0,840,115,1,0,0,0,841,842,5,46,0,0,842,117,1,0,0,0,843,844,7,15,0, + 0,844,845,7,12,0,0,845,846,7,13,0,0,846,847,7,2,0,0,847,848,7,3,0,0,848, + 119,1,0,0,0,849,850,7,15,0,0,850,851,7,1,0,0,851,852,7,6,0,0,852,853,7, + 2,0,0,853,854,7,5,0,0,854,121,1,0,0,0,855,856,7,1,0,0,856,857,7,9,0,0,857, + 123,1,0,0,0,858,859,7,1,0,0,859,860,7,2,0,0,860,125,1,0,0,0,861,862,7,13, + 0,0,862,863,7,12,0,0,863,864,7,2,0,0,864,865,7,5,0,0,865,127,1,0,0,0,866, + 867,7,13,0,0,867,868,7,1,0,0,868,869,7,18,0,0,869,870,7,3,0,0,870,129,1, + 0,0,0,871,872,5,40,0,0,872,131,1,0,0,0,873,874,7,9,0,0,874,875,7,7,0,0, + 875,876,7,5,0,0,876,133,1,0,0,0,877,878,7,9,0,0,878,879,7,20,0,0,879,880, + 7,13,0,0,880,881,7,13,0,0,881,135,1,0,0,0,882,883,7,9,0,0,883,884,7,20, + 0,0,884,885,7,13,0,0,885,886,7,13,0,0,886,887,7,2,0,0,887,137,1,0,0,0,888, + 889,7,7,0,0,889,890,7,6,0,0,890,139,1,0,0,0,891,892,5,63,0,0,892,141,1, + 0,0,0,893,894,7,6,0,0,894,895,7,13,0,0,895,896,7,1,0,0,896,897,7,18,0,0, + 897,898,7,3,0,0,898,143,1,0,0,0,899,900,5,41,0,0,900,145,1,0,0,0,901,902, + 7,5,0,0,902,903,7,6,0,0,903,904,7,20,0,0,904,905,7,3,0,0,905,147,1,0,0, + 0,906,907,5,61,0,0,907,908,5,61,0,0,908,149,1,0,0,0,909,910,5,61,0,0,910, + 911,5,126,0,0,911,151,1,0,0,0,912,913,5,33,0,0,913,914,5,61,0,0,914,153, + 1,0,0,0,915,916,5,60,0,0,916,155,1,0,0,0,917,918,5,60,0,0,918,919,5,61, + 0,0,919,157,1,0,0,0,920,921,5,62,0,0,921,159,1,0,0,0,922,923,5,62,0,0,923, + 924,5,61,0,0,924,161,1,0,0,0,925,926,5,43,0,0,926,163,1,0,0,0,927,928,5, + 45,0,0,928,165,1,0,0,0,929,930,5,42,0,0,930,167,1,0,0,0,931,932,5,47,0, + 0,932,169,1,0,0,0,933,934,5,37,0,0,934,171,1,0,0,0,935,936,3,46,15,0,936, + 937,1,0,0,0,937,938,6,78,13,0,938,173,1,0,0,0,939,942,3,140,62,0,940,943, + 3,76,30,0,941,943,3,90,37,0,942,940,1,0,0,0,942,941,1,0,0,0,943,947,1,0, + 0,0,944,946,3,92,38,0,945,944,1,0,0,0,946,949,1,0,0,0,947,945,1,0,0,0,947, + 948,1,0,0,0,948,957,1,0,0,0,949,947,1,0,0,0,950,952,3,140,62,0,951,953, + 3,74,29,0,952,951,1,0,0,0,953,954,1,0,0,0,954,952,1,0,0,0,954,955,1,0,0, + 0,955,957,1,0,0,0,956,939,1,0,0,0,956,950,1,0,0,0,957,175,1,0,0,0,958,959, + 5,91,0,0,959,960,1,0,0,0,960,961,6,80,0,0,961,962,6,80,0,0,962,177,1,0, + 0,0,963,964,5,93,0,0,964,965,1,0,0,0,965,966,6,81,12,0,966,967,6,81,12, + 0,967,179,1,0,0,0,968,972,3,76,30,0,969,971,3,92,38,0,970,969,1,0,0,0,971, + 974,1,0,0,0,972,970,1,0,0,0,972,973,1,0,0,0,973,985,1,0,0,0,974,972,1,0, + 0,0,975,978,3,90,37,0,976,978,3,84,34,0,977,975,1,0,0,0,977,976,1,0,0,0, + 978,980,1,0,0,0,979,981,3,92,38,0,980,979,1,0,0,0,981,982,1,0,0,0,982,980, + 1,0,0,0,982,983,1,0,0,0,983,985,1,0,0,0,984,968,1,0,0,0,984,977,1,0,0,0, + 985,181,1,0,0,0,986,988,3,86,35,0,987,989,3,88,36,0,988,987,1,0,0,0,989, + 990,1,0,0,0,990,988,1,0,0,0,990,991,1,0,0,0,991,992,1,0,0,0,992,993,3,86, + 35,0,993,183,1,0,0,0,994,995,3,182,83,0,995,185,1,0,0,0,996,997,3,66,25, + 0,997,998,1,0,0,0,998,999,6,85,11,0,999,187,1,0,0,0,1000,1001,3,68,26,0, + 1001,1002,1,0,0,0,1002,1003,6,86,11,0,1003,189,1,0,0,0,1004,1005,3,70,27, + 0,1005,1006,1,0,0,0,1006,1007,6,87,11,0,1007,191,1,0,0,0,1008,1009,3,176, + 80,0,1009,1010,1,0,0,0,1010,1011,6,88,14,0,1011,1012,6,88,15,0,1012,193, + 1,0,0,0,1013,1014,3,72,28,0,1014,1015,1,0,0,0,1015,1016,6,89,16,0,1016, + 1017,6,89,12,0,1017,195,1,0,0,0,1018,1019,3,70,27,0,1019,1020,1,0,0,0,1020, + 1021,6,90,11,0,1021,197,1,0,0,0,1022,1023,3,66,25,0,1023,1024,1,0,0,0,1024, + 1025,6,91,11,0,1025,199,1,0,0,0,1026,1027,3,68,26,0,1027,1028,1,0,0,0,1028, + 1029,6,92,11,0,1029,201,1,0,0,0,1030,1031,3,72,28,0,1031,1032,1,0,0,0,1032, + 1033,6,93,16,0,1033,1034,6,93,12,0,1034,203,1,0,0,0,1035,1036,3,176,80, + 0,1036,1037,1,0,0,0,1037,1038,6,94,14,0,1038,205,1,0,0,0,1039,1040,3,178, + 81,0,1040,1041,1,0,0,0,1041,1042,6,95,17,0,1042,207,1,0,0,0,1043,1044,3, + 110,47,0,1044,1045,1,0,0,0,1045,1046,6,96,18,0,1046,209,1,0,0,0,1047,1048, + 3,112,48,0,1048,1049,1,0,0,0,1049,1050,6,97,19,0,1050,211,1,0,0,0,1051, + 1052,3,106,45,0,1052,1053,1,0,0,0,1053,1054,6,98,20,0,1054,213,1,0,0,0, + 1055,1056,7,16,0,0,1056,1057,7,3,0,0,1057,1058,7,5,0,0,1058,1059,7,12,0, + 0,1059,1060,7,0,0,0,1060,1061,7,12,0,0,1061,1062,7,5,0,0,1062,1063,7,12, + 0,0,1063,215,1,0,0,0,1064,1068,8,33,0,0,1065,1066,5,47,0,0,1066,1068,8, + 34,0,0,1067,1064,1,0,0,0,1067,1065,1,0,0,0,1068,217,1,0,0,0,1069,1071,3, + 216,100,0,1070,1069,1,0,0,0,1071,1072,1,0,0,0,1072,1070,1,0,0,0,1072,1073, + 1,0,0,0,1073,219,1,0,0,0,1074,1075,3,218,101,0,1075,1076,1,0,0,0,1076,1077, + 6,102,21,0,1077,221,1,0,0,0,1078,1079,3,94,39,0,1079,1080,1,0,0,0,1080, + 1081,6,103,22,0,1081,223,1,0,0,0,1082,1083,3,66,25,0,1083,1084,1,0,0,0, + 1084,1085,6,104,11,0,1085,225,1,0,0,0,1086,1087,3,68,26,0,1087,1088,1,0, + 0,0,1088,1089,6,105,11,0,1089,227,1,0,0,0,1090,1091,3,70,27,0,1091,1092, + 1,0,0,0,1092,1093,6,106,11,0,1093,229,1,0,0,0,1094,1095,3,72,28,0,1095, + 1096,1,0,0,0,1096,1097,6,107,16,0,1097,1098,6,107,12,0,1098,231,1,0,0,0, + 1099,1100,3,116,50,0,1100,1101,1,0,0,0,1101,1102,6,108,23,0,1102,233,1, + 0,0,0,1103,1104,3,112,48,0,1104,1105,1,0,0,0,1105,1106,6,109,19,0,1106, + 235,1,0,0,0,1107,1108,4,110,8,0,1108,1109,3,140,62,0,1109,1110,1,0,0,0, + 1110,1111,6,110,24,0,1111,237,1,0,0,0,1112,1113,4,111,9,0,1113,1114,3,174, + 79,0,1114,1115,1,0,0,0,1115,1116,6,111,25,0,1116,239,1,0,0,0,1117,1122, + 3,76,30,0,1118,1122,3,74,29,0,1119,1122,3,90,37,0,1120,1122,3,166,75,0, + 1121,1117,1,0,0,0,1121,1118,1,0,0,0,1121,1119,1,0,0,0,1121,1120,1,0,0,0, + 1122,241,1,0,0,0,1123,1126,3,76,30,0,1124,1126,3,166,75,0,1125,1123,1,0, + 0,0,1125,1124,1,0,0,0,1126,1130,1,0,0,0,1127,1129,3,240,112,0,1128,1127, + 1,0,0,0,1129,1132,1,0,0,0,1130,1128,1,0,0,0,1130,1131,1,0,0,0,1131,1143, + 1,0,0,0,1132,1130,1,0,0,0,1133,1136,3,90,37,0,1134,1136,3,84,34,0,1135, + 1133,1,0,0,0,1135,1134,1,0,0,0,1136,1138,1,0,0,0,1137,1139,3,240,112,0, + 1138,1137,1,0,0,0,1139,1140,1,0,0,0,1140,1138,1,0,0,0,1140,1141,1,0,0,0, + 1141,1143,1,0,0,0,1142,1125,1,0,0,0,1142,1135,1,0,0,0,1143,243,1,0,0,0, + 1144,1147,3,242,113,0,1145,1147,3,182,83,0,1146,1144,1,0,0,0,1146,1145, + 1,0,0,0,1147,1148,1,0,0,0,1148,1146,1,0,0,0,1148,1149,1,0,0,0,1149,245, + 1,0,0,0,1150,1151,3,66,25,0,1151,1152,1,0,0,0,1152,1153,6,115,11,0,1153, + 247,1,0,0,0,1154,1155,3,68,26,0,1155,1156,1,0,0,0,1156,1157,6,116,11,0, + 1157,249,1,0,0,0,1158,1159,3,70,27,0,1159,1160,1,0,0,0,1160,1161,6,117, + 11,0,1161,251,1,0,0,0,1162,1163,3,72,28,0,1163,1164,1,0,0,0,1164,1165,6, + 118,16,0,1165,1166,6,118,12,0,1166,253,1,0,0,0,1167,1168,3,106,45,0,1168, + 1169,1,0,0,0,1169,1170,6,119,20,0,1170,255,1,0,0,0,1171,1172,3,112,48,0, + 1172,1173,1,0,0,0,1173,1174,6,120,19,0,1174,257,1,0,0,0,1175,1176,3,116, + 50,0,1176,1177,1,0,0,0,1177,1178,6,121,23,0,1178,259,1,0,0,0,1179,1180, + 4,122,10,0,1180,1181,3,140,62,0,1181,1182,1,0,0,0,1182,1183,6,122,24,0, + 1183,261,1,0,0,0,1184,1185,4,123,11,0,1185,1186,3,174,79,0,1186,1187,1, + 0,0,0,1187,1188,6,123,25,0,1188,263,1,0,0,0,1189,1190,7,12,0,0,1190,1191, + 7,2,0,0,1191,265,1,0,0,0,1192,1193,3,244,114,0,1193,1194,1,0,0,0,1194,1195, + 6,125,26,0,1195,267,1,0,0,0,1196,1197,3,66,25,0,1197,1198,1,0,0,0,1198, + 1199,6,126,11,0,1199,269,1,0,0,0,1200,1201,3,68,26,0,1201,1202,1,0,0,0, + 1202,1203,6,127,11,0,1203,271,1,0,0,0,1204,1205,3,70,27,0,1205,1206,1,0, + 0,0,1206,1207,6,128,11,0,1207,273,1,0,0,0,1208,1209,3,72,28,0,1209,1210, + 1,0,0,0,1210,1211,6,129,16,0,1211,1212,6,129,12,0,1212,275,1,0,0,0,1213, + 1214,3,176,80,0,1214,1215,1,0,0,0,1215,1216,6,130,14,0,1216,1217,6,130, + 27,0,1217,277,1,0,0,0,1218,1219,7,7,0,0,1219,1220,7,9,0,0,1220,1221,1,0, + 0,0,1221,1222,6,131,28,0,1222,279,1,0,0,0,1223,1224,7,19,0,0,1224,1225, + 7,1,0,0,1225,1226,7,5,0,0,1226,1227,7,10,0,0,1227,1228,1,0,0,0,1228,1229, + 6,132,28,0,1229,281,1,0,0,0,1230,1231,8,35,0,0,1231,283,1,0,0,0,1232,1234, + 3,282,133,0,1233,1232,1,0,0,0,1234,1235,1,0,0,0,1235,1233,1,0,0,0,1235, + 1236,1,0,0,0,1236,1237,1,0,0,0,1237,1238,3,110,47,0,1238,1240,1,0,0,0,1239, + 1233,1,0,0,0,1239,1240,1,0,0,0,1240,1242,1,0,0,0,1241,1243,3,282,133,0, + 1242,1241,1,0,0,0,1243,1244,1,0,0,0,1244,1242,1,0,0,0,1244,1245,1,0,0,0, + 1245,285,1,0,0,0,1246,1247,3,284,134,0,1247,1248,1,0,0,0,1248,1249,6,135, + 29,0,1249,287,1,0,0,0,1250,1251,3,66,25,0,1251,1252,1,0,0,0,1252,1253,6, + 136,11,0,1253,289,1,0,0,0,1254,1255,3,68,26,0,1255,1256,1,0,0,0,1256,1257, + 6,137,11,0,1257,291,1,0,0,0,1258,1259,3,70,27,0,1259,1260,1,0,0,0,1260, + 1261,6,138,11,0,1261,293,1,0,0,0,1262,1263,3,72,28,0,1263,1264,1,0,0,0, + 1264,1265,6,139,16,0,1265,1266,6,139,12,0,1266,1267,6,139,12,0,1267,295, + 1,0,0,0,1268,1269,3,106,45,0,1269,1270,1,0,0,0,1270,1271,6,140,20,0,1271, + 297,1,0,0,0,1272,1273,3,112,48,0,1273,1274,1,0,0,0,1274,1275,6,141,19,0, + 1275,299,1,0,0,0,1276,1277,3,116,50,0,1277,1278,1,0,0,0,1278,1279,6,142, + 23,0,1279,301,1,0,0,0,1280,1281,3,280,132,0,1281,1282,1,0,0,0,1282,1283, + 6,143,30,0,1283,303,1,0,0,0,1284,1285,3,244,114,0,1285,1286,1,0,0,0,1286, + 1287,6,144,26,0,1287,305,1,0,0,0,1288,1289,3,184,84,0,1289,1290,1,0,0,0, + 1290,1291,6,145,31,0,1291,307,1,0,0,0,1292,1293,4,146,12,0,1293,1294,3, + 140,62,0,1294,1295,1,0,0,0,1295,1296,6,146,24,0,1296,309,1,0,0,0,1297,1298, + 4,147,13,0,1298,1299,3,174,79,0,1299,1300,1,0,0,0,1300,1301,6,147,25,0, + 1301,311,1,0,0,0,1302,1303,3,66,25,0,1303,1304,1,0,0,0,1304,1305,6,148, + 11,0,1305,313,1,0,0,0,1306,1307,3,68,26,0,1307,1308,1,0,0,0,1308,1309,6, + 149,11,0,1309,315,1,0,0,0,1310,1311,3,70,27,0,1311,1312,1,0,0,0,1312,1313, + 6,150,11,0,1313,317,1,0,0,0,1314,1315,3,72,28,0,1315,1316,1,0,0,0,1316, + 1317,6,151,16,0,1317,1318,6,151,12,0,1318,319,1,0,0,0,1319,1320,3,116,50, + 0,1320,1321,1,0,0,0,1321,1322,6,152,23,0,1322,321,1,0,0,0,1323,1324,4,153, + 14,0,1324,1325,3,140,62,0,1325,1326,1,0,0,0,1326,1327,6,153,24,0,1327,323, + 1,0,0,0,1328,1329,4,154,15,0,1329,1330,3,174,79,0,1330,1331,1,0,0,0,1331, + 1332,6,154,25,0,1332,325,1,0,0,0,1333,1334,3,184,84,0,1334,1335,1,0,0,0, + 1335,1336,6,155,31,0,1336,327,1,0,0,0,1337,1338,3,180,82,0,1338,1339,1, + 0,0,0,1339,1340,6,156,32,0,1340,329,1,0,0,0,1341,1342,3,66,25,0,1342,1343, + 1,0,0,0,1343,1344,6,157,11,0,1344,331,1,0,0,0,1345,1346,3,68,26,0,1346, + 1347,1,0,0,0,1347,1348,6,158,11,0,1348,333,1,0,0,0,1349,1350,3,70,27,0, + 1350,1351,1,0,0,0,1351,1352,6,159,11,0,1352,335,1,0,0,0,1353,1354,3,72, + 28,0,1354,1355,1,0,0,0,1355,1356,6,160,16,0,1356,1357,6,160,12,0,1357,337, + 1,0,0,0,1358,1359,7,1,0,0,1359,1360,7,9,0,0,1360,1361,7,15,0,0,1361,1362, + 7,7,0,0,1362,339,1,0,0,0,1363,1364,3,66,25,0,1364,1365,1,0,0,0,1365,1366, + 6,162,11,0,1366,341,1,0,0,0,1367,1368,3,68,26,0,1368,1369,1,0,0,0,1369, + 1370,6,163,11,0,1370,343,1,0,0,0,1371,1372,3,70,27,0,1372,1373,1,0,0,0, + 1373,1374,6,164,11,0,1374,345,1,0,0,0,1375,1376,3,178,81,0,1376,1377,1, + 0,0,0,1377,1378,6,165,17,0,1378,1379,6,165,12,0,1379,347,1,0,0,0,1380,1381, + 3,110,47,0,1381,1382,1,0,0,0,1382,1383,6,166,18,0,1383,349,1,0,0,0,1384, + 1390,3,84,34,0,1385,1390,3,74,29,0,1386,1390,3,116,50,0,1387,1390,3,76, + 30,0,1388,1390,3,90,37,0,1389,1384,1,0,0,0,1389,1385,1,0,0,0,1389,1386, + 1,0,0,0,1389,1387,1,0,0,0,1389,1388,1,0,0,0,1390,1391,1,0,0,0,1391,1389, + 1,0,0,0,1391,1392,1,0,0,0,1392,351,1,0,0,0,1393,1394,3,66,25,0,1394,1395, + 1,0,0,0,1395,1396,6,168,11,0,1396,353,1,0,0,0,1397,1398,3,68,26,0,1398, + 1399,1,0,0,0,1399,1400,6,169,11,0,1400,355,1,0,0,0,1401,1402,3,70,27,0, + 1402,1403,1,0,0,0,1403,1404,6,170,11,0,1404,357,1,0,0,0,1405,1406,3,72, + 28,0,1406,1407,1,0,0,0,1407,1408,6,171,16,0,1408,1409,6,171,12,0,1409,359, + 1,0,0,0,1410,1411,3,110,47,0,1411,1412,1,0,0,0,1412,1413,6,172,18,0,1413, + 361,1,0,0,0,1414,1415,3,112,48,0,1415,1416,1,0,0,0,1416,1417,6,173,19,0, + 1417,363,1,0,0,0,1418,1419,3,116,50,0,1419,1420,1,0,0,0,1420,1421,6,174, + 23,0,1421,365,1,0,0,0,1422,1423,3,278,131,0,1423,1424,1,0,0,0,1424,1425, + 6,175,33,0,1425,1426,6,175,34,0,1426,367,1,0,0,0,1427,1428,3,218,101,0, + 1428,1429,1,0,0,0,1429,1430,6,176,21,0,1430,369,1,0,0,0,1431,1432,3,94, + 39,0,1432,1433,1,0,0,0,1433,1434,6,177,22,0,1434,371,1,0,0,0,1435,1436, + 3,66,25,0,1436,1437,1,0,0,0,1437,1438,6,178,11,0,1438,373,1,0,0,0,1439, + 1440,3,68,26,0,1440,1441,1,0,0,0,1441,1442,6,179,11,0,1442,375,1,0,0,0, + 1443,1444,3,70,27,0,1444,1445,1,0,0,0,1445,1446,6,180,11,0,1446,377,1,0, + 0,0,1447,1448,3,72,28,0,1448,1449,1,0,0,0,1449,1450,6,181,16,0,1450,1451, + 6,181,12,0,1451,1452,6,181,12,0,1452,379,1,0,0,0,1453,1454,3,112,48,0,1454, + 1455,1,0,0,0,1455,1456,6,182,19,0,1456,381,1,0,0,0,1457,1458,3,116,50,0, + 1458,1459,1,0,0,0,1459,1460,6,183,23,0,1460,383,1,0,0,0,1461,1462,3,244, + 114,0,1462,1463,1,0,0,0,1463,1464,6,184,26,0,1464,385,1,0,0,0,1465,1466, + 3,66,25,0,1466,1467,1,0,0,0,1467,1468,6,185,11,0,1468,387,1,0,0,0,1469, + 1470,3,68,26,0,1470,1471,1,0,0,0,1471,1472,6,186,11,0,1472,389,1,0,0,0, + 1473,1474,3,70,27,0,1474,1475,1,0,0,0,1475,1476,6,187,11,0,1476,391,1,0, + 0,0,1477,1478,3,72,28,0,1478,1479,1,0,0,0,1479,1480,6,188,16,0,1480,1481, + 6,188,12,0,1481,393,1,0,0,0,1482,1483,3,54,19,0,1483,1484,1,0,0,0,1484, + 1485,6,189,35,0,1485,395,1,0,0,0,1486,1487,3,264,124,0,1487,1488,1,0,0, + 0,1488,1489,6,190,36,0,1489,397,1,0,0,0,1490,1491,3,278,131,0,1491,1492, + 1,0,0,0,1492,1493,6,191,33,0,1493,1494,6,191,12,0,1494,1495,6,191,0,0,1495, + 399,1,0,0,0,1496,1497,7,20,0,0,1497,1498,7,2,0,0,1498,1499,7,1,0,0,1499, + 1500,7,9,0,0,1500,1501,7,17,0,0,1501,1502,1,0,0,0,1502,1503,6,192,12,0, + 1503,1504,6,192,0,0,1504,401,1,0,0,0,1505,1506,3,180,82,0,1506,1507,1,0, + 0,0,1507,1508,6,193,32,0,1508,403,1,0,0,0,1509,1510,3,184,84,0,1510,1511, + 1,0,0,0,1511,1512,6,194,31,0,1512,405,1,0,0,0,1513,1514,3,66,25,0,1514, + 1515,1,0,0,0,1515,1516,6,195,11,0,1516,407,1,0,0,0,1517,1518,3,68,26,0, + 1518,1519,1,0,0,0,1519,1520,6,196,11,0,1520,409,1,0,0,0,1521,1522,3,70, + 27,0,1522,1523,1,0,0,0,1523,1524,6,197,11,0,1524,411,1,0,0,0,1525,1526, + 3,72,28,0,1526,1527,1,0,0,0,1527,1528,6,198,16,0,1528,1529,6,198,12,0,1529, + 413,1,0,0,0,1530,1531,3,218,101,0,1531,1532,1,0,0,0,1532,1533,6,199,21, + 0,1533,1534,6,199,12,0,1534,1535,6,199,37,0,1535,415,1,0,0,0,1536,1537, + 3,94,39,0,1537,1538,1,0,0,0,1538,1539,6,200,22,0,1539,1540,6,200,12,0,1540, + 1541,6,200,37,0,1541,417,1,0,0,0,1542,1543,3,66,25,0,1543,1544,1,0,0,0, + 1544,1545,6,201,11,0,1545,419,1,0,0,0,1546,1547,3,68,26,0,1547,1548,1,0, + 0,0,1548,1549,6,202,11,0,1549,421,1,0,0,0,1550,1551,3,70,27,0,1551,1552, + 1,0,0,0,1552,1553,6,203,11,0,1553,423,1,0,0,0,1554,1555,3,110,47,0,1555, + 1556,1,0,0,0,1556,1557,6,204,18,0,1557,1558,6,204,12,0,1558,1559,6,204, + 9,0,1559,425,1,0,0,0,1560,1561,3,112,48,0,1561,1562,1,0,0,0,1562,1563,6, + 205,19,0,1563,1564,6,205,12,0,1564,1565,6,205,9,0,1565,427,1,0,0,0,1566, + 1567,3,66,25,0,1567,1568,1,0,0,0,1568,1569,6,206,11,0,1569,429,1,0,0,0, + 1570,1571,3,68,26,0,1571,1572,1,0,0,0,1572,1573,6,207,11,0,1573,431,1,0, + 0,0,1574,1575,3,70,27,0,1575,1576,1,0,0,0,1576,1577,6,208,11,0,1577,433, + 1,0,0,0,1578,1579,3,184,84,0,1579,1580,1,0,0,0,1580,1581,6,209,12,0,1581, + 1582,6,209,0,0,1582,1583,6,209,31,0,1583,435,1,0,0,0,1584,1585,3,180,82, + 0,1585,1586,1,0,0,0,1586,1587,6,210,12,0,1587,1588,6,210,0,0,1588,1589, + 6,210,32,0,1589,437,1,0,0,0,1590,1591,3,100,42,0,1591,1592,1,0,0,0,1592, + 1593,6,211,12,0,1593,1594,6,211,0,0,1594,1595,6,211,38,0,1595,439,1,0,0, + 0,1596,1597,3,72,28,0,1597,1598,1,0,0,0,1598,1599,6,212,16,0,1599,1600, + 6,212,12,0,1600,441,1,0,0,0,66,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,655, + 665,669,672,681,683,694,713,718,727,734,739,741,752,760,763,765,770,775, + 781,788,793,799,802,810,814,942,947,954,956,972,977,982,984,990,1067,1072, + 1121,1125,1130,1135,1140,1142,1146,1148,1235,1239,1244,1389,1391,39,5,1, + 0,5,4,0,5,6,0,5,2,0,5,3,0,5,8,0,5,5,0,5,9,0,5,11,0,5,14,0,5,13,0,0,1,0, + 4,0,0,7,16,0,7,70,0,5,0,0,7,29,0,7,71,0,7,38,0,7,39,0,7,36,0,7,81,0,7,30, + 0,7,41,0,7,53,0,7,69,0,7,85,0,5,10,0,5,7,0,7,95,0,7,94,0,7,73,0,7,72,0, + 7,93,0,5,12,0,7,20,0,7,89,0,5,15,0,7,33,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/kbn-esql-ast/src/antlr/esql_parser.g4 b/packages/kbn-esql-ast/src/antlr/esql_parser.g4 index 6a76e32d28f36..0857f14f9d0f0 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_parser.g4 +++ b/packages/kbn-esql-ast/src/antlr/esql_parser.g4 @@ -56,6 +56,7 @@ processingCommand // in development | {this.isDevVersion()}? inlinestatsCommand | {this.isDevVersion()}? lookupCommand + | {this.isDevVersion()}? joinCommand ; whereCommand @@ -70,7 +71,7 @@ booleanExpression | left=booleanExpression operator=OR right=booleanExpression #logicalBinary | valueExpression (NOT)? IN LP valueExpression (COMMA valueExpression)* RP #logicalIn | valueExpression IS NOT? NULL #isNull - | {this.isDevVersion()}? matchBooleanExpression #matchExpression + | matchBooleanExpression #matchExpression ; regexBooleanExpression @@ -323,4 +324,20 @@ lookupCommand inlinestatsCommand : DEV_INLINESTATS stats=aggFields (BY grouping=fields)? + ; + +joinCommand + : type=(DEV_JOIN_LOOKUP | DEV_JOIN_LEFT | DEV_JOIN_RIGHT)? DEV_JOIN joinTarget joinCondition + ; + +joinTarget + : index=identifier (AS alias=identifier)? + ; + +joinCondition + : ON joinPredicate (COMMA joinPredicate)* + ; + +joinPredicate + : valueExpression ; \ No newline at end of file diff --git a/packages/kbn-esql-ast/src/antlr/esql_parser.interp b/packages/kbn-esql-ast/src/antlr/esql_parser.interp index a2b339f378f12..50493f584fe4c 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_parser.interp +++ b/packages/kbn-esql-ast/src/antlr/esql_parser.interp @@ -23,7 +23,11 @@ null null null null -':' +null +null +null +null +null '|' null null @@ -33,6 +37,7 @@ null 'asc' '=' '::' +':' ',' 'desc' '.' @@ -113,6 +118,10 @@ null null null null +'USING' +null +null +null null null null @@ -141,11 +150,15 @@ WHERE DEV_INLINESTATS DEV_LOOKUP DEV_METRICS +DEV_JOIN +DEV_JOIN_FULL +DEV_JOIN_LEFT +DEV_JOIN_RIGHT +DEV_JOIN_LOOKUP UNKNOWN_CMD LINE_COMMENT MULTILINE_COMMENT WS -COLON PIPE QUOTED_STRING INTEGER_LITERAL @@ -155,6 +168,7 @@ AND ASC ASSIGN CAST_OP +COLON COMMA DESC DOT @@ -235,6 +249,10 @@ LOOKUP_WS LOOKUP_FIELD_LINE_COMMENT LOOKUP_FIELD_MULTILINE_COMMENT LOOKUP_FIELD_WS +USING +JOIN_LINE_COMMENT +JOIN_MULTILINE_COMMENT +JOIN_WS METRICS_LINE_COMMENT METRICS_MULTILINE_COMMENT METRICS_WS @@ -305,7 +323,11 @@ enrichCommand enrichWithClause lookupCommand inlinestatsCommand +joinCommand +joinTarget +joinCondition +joinPredicate atn: -[4, 1, 119, 603, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 134, 8, 1, 10, 1, 12, 1, 137, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 145, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 163, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 175, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 182, 8, 5, 10, 5, 12, 5, 185, 9, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 192, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 198, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 206, 8, 5, 10, 5, 12, 5, 209, 9, 5, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 220, 8, 6, 1, 6, 1, 6, 1, 6, 3, 6, 225, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 236, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 242, 8, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 5, 9, 250, 8, 9, 10, 9, 12, 9, 253, 9, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 3, 10, 263, 8, 10, 1, 10, 1, 10, 1, 10, 5, 10, 268, 8, 10, 10, 10, 12, 10, 271, 9, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 5, 11, 279, 8, 11, 10, 11, 12, 11, 282, 9, 11, 3, 11, 284, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 298, 8, 15, 10, 15, 12, 15, 301, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 306, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 314, 8, 17, 10, 17, 12, 17, 317, 9, 17, 1, 17, 3, 17, 320, 8, 17, 1, 18, 1, 18, 1, 18, 3, 18, 325, 8, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 335, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 341, 8, 22, 10, 22, 12, 22, 344, 9, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 354, 8, 24, 10, 24, 12, 24, 357, 9, 24, 1, 24, 3, 24, 360, 8, 24, 1, 24, 1, 24, 3, 24, 364, 8, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 3, 26, 371, 8, 26, 1, 26, 1, 26, 3, 26, 375, 8, 26, 1, 27, 1, 27, 1, 27, 5, 27, 380, 8, 27, 10, 27, 12, 27, 383, 9, 27, 1, 28, 1, 28, 1, 28, 3, 28, 388, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 393, 8, 29, 10, 29, 12, 29, 396, 9, 29, 1, 30, 1, 30, 1, 30, 5, 30, 401, 8, 30, 10, 30, 12, 30, 404, 9, 30, 1, 31, 1, 31, 1, 31, 5, 31, 409, 8, 31, 10, 31, 12, 31, 412, 9, 31, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 3, 33, 419, 8, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 434, 8, 34, 10, 34, 12, 34, 437, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 445, 8, 34, 10, 34, 12, 34, 448, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 456, 8, 34, 10, 34, 12, 34, 459, 9, 34, 1, 34, 1, 34, 3, 34, 463, 8, 34, 1, 35, 1, 35, 3, 35, 467, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 472, 8, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 481, 8, 38, 10, 38, 12, 38, 484, 9, 38, 1, 39, 1, 39, 3, 39, 488, 8, 39, 1, 39, 1, 39, 3, 39, 492, 8, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 504, 8, 42, 10, 42, 12, 42, 507, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 517, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 5, 47, 529, 8, 47, 10, 47, 12, 47, 532, 9, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 3, 50, 542, 8, 50, 1, 51, 3, 51, 545, 8, 51, 1, 51, 1, 51, 1, 52, 3, 52, 550, 8, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 572, 8, 58, 1, 58, 1, 58, 1, 58, 1, 58, 5, 58, 578, 8, 58, 10, 58, 12, 58, 581, 9, 58, 3, 58, 583, 8, 58, 1, 59, 1, 59, 1, 59, 3, 59, 588, 8, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 601, 8, 61, 1, 61, 0, 4, 2, 10, 18, 20, 62, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 0, 8, 1, 0, 59, 60, 1, 0, 61, 63, 2, 0, 26, 26, 76, 76, 1, 0, 67, 68, 2, 0, 31, 31, 35, 35, 2, 0, 38, 38, 41, 41, 2, 0, 37, 37, 51, 51, 2, 0, 52, 52, 54, 58, 628, 0, 124, 1, 0, 0, 0, 2, 127, 1, 0, 0, 0, 4, 144, 1, 0, 0, 0, 6, 162, 1, 0, 0, 0, 8, 164, 1, 0, 0, 0, 10, 197, 1, 0, 0, 0, 12, 224, 1, 0, 0, 0, 14, 226, 1, 0, 0, 0, 16, 235, 1, 0, 0, 0, 18, 241, 1, 0, 0, 0, 20, 262, 1, 0, 0, 0, 22, 272, 1, 0, 0, 0, 24, 287, 1, 0, 0, 0, 26, 289, 1, 0, 0, 0, 28, 291, 1, 0, 0, 0, 30, 294, 1, 0, 0, 0, 32, 305, 1, 0, 0, 0, 34, 309, 1, 0, 0, 0, 36, 324, 1, 0, 0, 0, 38, 328, 1, 0, 0, 0, 40, 330, 1, 0, 0, 0, 42, 334, 1, 0, 0, 0, 44, 336, 1, 0, 0, 0, 46, 345, 1, 0, 0, 0, 48, 349, 1, 0, 0, 0, 50, 365, 1, 0, 0, 0, 52, 368, 1, 0, 0, 0, 54, 376, 1, 0, 0, 0, 56, 384, 1, 0, 0, 0, 58, 389, 1, 0, 0, 0, 60, 397, 1, 0, 0, 0, 62, 405, 1, 0, 0, 0, 64, 413, 1, 0, 0, 0, 66, 418, 1, 0, 0, 0, 68, 462, 1, 0, 0, 0, 70, 466, 1, 0, 0, 0, 72, 471, 1, 0, 0, 0, 74, 473, 1, 0, 0, 0, 76, 476, 1, 0, 0, 0, 78, 485, 1, 0, 0, 0, 80, 493, 1, 0, 0, 0, 82, 496, 1, 0, 0, 0, 84, 499, 1, 0, 0, 0, 86, 508, 1, 0, 0, 0, 88, 512, 1, 0, 0, 0, 90, 518, 1, 0, 0, 0, 92, 522, 1, 0, 0, 0, 94, 525, 1, 0, 0, 0, 96, 533, 1, 0, 0, 0, 98, 537, 1, 0, 0, 0, 100, 541, 1, 0, 0, 0, 102, 544, 1, 0, 0, 0, 104, 549, 1, 0, 0, 0, 106, 553, 1, 0, 0, 0, 108, 555, 1, 0, 0, 0, 110, 557, 1, 0, 0, 0, 112, 560, 1, 0, 0, 0, 114, 564, 1, 0, 0, 0, 116, 567, 1, 0, 0, 0, 118, 587, 1, 0, 0, 0, 120, 591, 1, 0, 0, 0, 122, 596, 1, 0, 0, 0, 124, 125, 3, 2, 1, 0, 125, 126, 5, 0, 0, 1, 126, 1, 1, 0, 0, 0, 127, 128, 6, 1, -1, 0, 128, 129, 3, 4, 2, 0, 129, 135, 1, 0, 0, 0, 130, 131, 10, 1, 0, 0, 131, 132, 5, 25, 0, 0, 132, 134, 3, 6, 3, 0, 133, 130, 1, 0, 0, 0, 134, 137, 1, 0, 0, 0, 135, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 3, 1, 0, 0, 0, 137, 135, 1, 0, 0, 0, 138, 145, 3, 110, 55, 0, 139, 145, 3, 34, 17, 0, 140, 145, 3, 28, 14, 0, 141, 145, 3, 114, 57, 0, 142, 143, 4, 2, 1, 0, 143, 145, 3, 48, 24, 0, 144, 138, 1, 0, 0, 0, 144, 139, 1, 0, 0, 0, 144, 140, 1, 0, 0, 0, 144, 141, 1, 0, 0, 0, 144, 142, 1, 0, 0, 0, 145, 5, 1, 0, 0, 0, 146, 163, 3, 50, 25, 0, 147, 163, 3, 8, 4, 0, 148, 163, 3, 80, 40, 0, 149, 163, 3, 74, 37, 0, 150, 163, 3, 52, 26, 0, 151, 163, 3, 76, 38, 0, 152, 163, 3, 82, 41, 0, 153, 163, 3, 84, 42, 0, 154, 163, 3, 88, 44, 0, 155, 163, 3, 90, 45, 0, 156, 163, 3, 116, 58, 0, 157, 163, 3, 92, 46, 0, 158, 159, 4, 3, 2, 0, 159, 163, 3, 122, 61, 0, 160, 161, 4, 3, 3, 0, 161, 163, 3, 120, 60, 0, 162, 146, 1, 0, 0, 0, 162, 147, 1, 0, 0, 0, 162, 148, 1, 0, 0, 0, 162, 149, 1, 0, 0, 0, 162, 150, 1, 0, 0, 0, 162, 151, 1, 0, 0, 0, 162, 152, 1, 0, 0, 0, 162, 153, 1, 0, 0, 0, 162, 154, 1, 0, 0, 0, 162, 155, 1, 0, 0, 0, 162, 156, 1, 0, 0, 0, 162, 157, 1, 0, 0, 0, 162, 158, 1, 0, 0, 0, 162, 160, 1, 0, 0, 0, 163, 7, 1, 0, 0, 0, 164, 165, 5, 16, 0, 0, 165, 166, 3, 10, 5, 0, 166, 9, 1, 0, 0, 0, 167, 168, 6, 5, -1, 0, 168, 169, 5, 44, 0, 0, 169, 198, 3, 10, 5, 8, 170, 198, 3, 16, 8, 0, 171, 198, 3, 12, 6, 0, 172, 174, 3, 16, 8, 0, 173, 175, 5, 44, 0, 0, 174, 173, 1, 0, 0, 0, 174, 175, 1, 0, 0, 0, 175, 176, 1, 0, 0, 0, 176, 177, 5, 39, 0, 0, 177, 178, 5, 43, 0, 0, 178, 183, 3, 16, 8, 0, 179, 180, 5, 34, 0, 0, 180, 182, 3, 16, 8, 0, 181, 179, 1, 0, 0, 0, 182, 185, 1, 0, 0, 0, 183, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 186, 1, 0, 0, 0, 185, 183, 1, 0, 0, 0, 186, 187, 5, 50, 0, 0, 187, 198, 1, 0, 0, 0, 188, 189, 3, 16, 8, 0, 189, 191, 5, 40, 0, 0, 190, 192, 5, 44, 0, 0, 191, 190, 1, 0, 0, 0, 191, 192, 1, 0, 0, 0, 192, 193, 1, 0, 0, 0, 193, 194, 5, 45, 0, 0, 194, 198, 1, 0, 0, 0, 195, 196, 4, 5, 4, 0, 196, 198, 3, 14, 7, 0, 197, 167, 1, 0, 0, 0, 197, 170, 1, 0, 0, 0, 197, 171, 1, 0, 0, 0, 197, 172, 1, 0, 0, 0, 197, 188, 1, 0, 0, 0, 197, 195, 1, 0, 0, 0, 198, 207, 1, 0, 0, 0, 199, 200, 10, 5, 0, 0, 200, 201, 5, 30, 0, 0, 201, 206, 3, 10, 5, 6, 202, 203, 10, 4, 0, 0, 203, 204, 5, 47, 0, 0, 204, 206, 3, 10, 5, 5, 205, 199, 1, 0, 0, 0, 205, 202, 1, 0, 0, 0, 206, 209, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 207, 208, 1, 0, 0, 0, 208, 11, 1, 0, 0, 0, 209, 207, 1, 0, 0, 0, 210, 212, 3, 16, 8, 0, 211, 213, 5, 44, 0, 0, 212, 211, 1, 0, 0, 0, 212, 213, 1, 0, 0, 0, 213, 214, 1, 0, 0, 0, 214, 215, 5, 42, 0, 0, 215, 216, 3, 106, 53, 0, 216, 225, 1, 0, 0, 0, 217, 219, 3, 16, 8, 0, 218, 220, 5, 44, 0, 0, 219, 218, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0, 220, 221, 1, 0, 0, 0, 221, 222, 5, 49, 0, 0, 222, 223, 3, 106, 53, 0, 223, 225, 1, 0, 0, 0, 224, 210, 1, 0, 0, 0, 224, 217, 1, 0, 0, 0, 225, 13, 1, 0, 0, 0, 226, 227, 3, 58, 29, 0, 227, 228, 5, 24, 0, 0, 228, 229, 3, 68, 34, 0, 229, 15, 1, 0, 0, 0, 230, 236, 3, 18, 9, 0, 231, 232, 3, 18, 9, 0, 232, 233, 3, 108, 54, 0, 233, 234, 3, 18, 9, 0, 234, 236, 1, 0, 0, 0, 235, 230, 1, 0, 0, 0, 235, 231, 1, 0, 0, 0, 236, 17, 1, 0, 0, 0, 237, 238, 6, 9, -1, 0, 238, 242, 3, 20, 10, 0, 239, 240, 7, 0, 0, 0, 240, 242, 3, 18, 9, 3, 241, 237, 1, 0, 0, 0, 241, 239, 1, 0, 0, 0, 242, 251, 1, 0, 0, 0, 243, 244, 10, 2, 0, 0, 244, 245, 7, 1, 0, 0, 245, 250, 3, 18, 9, 3, 246, 247, 10, 1, 0, 0, 247, 248, 7, 0, 0, 0, 248, 250, 3, 18, 9, 2, 249, 243, 1, 0, 0, 0, 249, 246, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 19, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 6, 10, -1, 0, 255, 263, 3, 68, 34, 0, 256, 263, 3, 58, 29, 0, 257, 263, 3, 22, 11, 0, 258, 259, 5, 43, 0, 0, 259, 260, 3, 10, 5, 0, 260, 261, 5, 50, 0, 0, 261, 263, 1, 0, 0, 0, 262, 254, 1, 0, 0, 0, 262, 256, 1, 0, 0, 0, 262, 257, 1, 0, 0, 0, 262, 258, 1, 0, 0, 0, 263, 269, 1, 0, 0, 0, 264, 265, 10, 1, 0, 0, 265, 266, 5, 33, 0, 0, 266, 268, 3, 26, 13, 0, 267, 264, 1, 0, 0, 0, 268, 271, 1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 21, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 272, 273, 3, 24, 12, 0, 273, 283, 5, 43, 0, 0, 274, 284, 5, 61, 0, 0, 275, 280, 3, 10, 5, 0, 276, 277, 5, 34, 0, 0, 277, 279, 3, 10, 5, 0, 278, 276, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280, 278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 284, 1, 0, 0, 0, 282, 280, 1, 0, 0, 0, 283, 274, 1, 0, 0, 0, 283, 275, 1, 0, 0, 0, 283, 284, 1, 0, 0, 0, 284, 285, 1, 0, 0, 0, 285, 286, 5, 50, 0, 0, 286, 23, 1, 0, 0, 0, 287, 288, 3, 72, 36, 0, 288, 25, 1, 0, 0, 0, 289, 290, 3, 64, 32, 0, 290, 27, 1, 0, 0, 0, 291, 292, 5, 12, 0, 0, 292, 293, 3, 30, 15, 0, 293, 29, 1, 0, 0, 0, 294, 299, 3, 32, 16, 0, 295, 296, 5, 34, 0, 0, 296, 298, 3, 32, 16, 0, 297, 295, 1, 0, 0, 0, 298, 301, 1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 31, 1, 0, 0, 0, 301, 299, 1, 0, 0, 0, 302, 303, 3, 58, 29, 0, 303, 304, 5, 32, 0, 0, 304, 306, 1, 0, 0, 0, 305, 302, 1, 0, 0, 0, 305, 306, 1, 0, 0, 0, 306, 307, 1, 0, 0, 0, 307, 308, 3, 10, 5, 0, 308, 33, 1, 0, 0, 0, 309, 310, 5, 6, 0, 0, 310, 315, 3, 36, 18, 0, 311, 312, 5, 34, 0, 0, 312, 314, 3, 36, 18, 0, 313, 311, 1, 0, 0, 0, 314, 317, 1, 0, 0, 0, 315, 313, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 319, 1, 0, 0, 0, 317, 315, 1, 0, 0, 0, 318, 320, 3, 42, 21, 0, 319, 318, 1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 35, 1, 0, 0, 0, 321, 322, 3, 38, 19, 0, 322, 323, 5, 24, 0, 0, 323, 325, 1, 0, 0, 0, 324, 321, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 326, 1, 0, 0, 0, 326, 327, 3, 40, 20, 0, 327, 37, 1, 0, 0, 0, 328, 329, 5, 76, 0, 0, 329, 39, 1, 0, 0, 0, 330, 331, 7, 2, 0, 0, 331, 41, 1, 0, 0, 0, 332, 335, 3, 44, 22, 0, 333, 335, 3, 46, 23, 0, 334, 332, 1, 0, 0, 0, 334, 333, 1, 0, 0, 0, 335, 43, 1, 0, 0, 0, 336, 337, 5, 75, 0, 0, 337, 342, 5, 76, 0, 0, 338, 339, 5, 34, 0, 0, 339, 341, 5, 76, 0, 0, 340, 338, 1, 0, 0, 0, 341, 344, 1, 0, 0, 0, 342, 340, 1, 0, 0, 0, 342, 343, 1, 0, 0, 0, 343, 45, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 345, 346, 5, 65, 0, 0, 346, 347, 3, 44, 22, 0, 347, 348, 5, 66, 0, 0, 348, 47, 1, 0, 0, 0, 349, 350, 5, 19, 0, 0, 350, 355, 3, 36, 18, 0, 351, 352, 5, 34, 0, 0, 352, 354, 3, 36, 18, 0, 353, 351, 1, 0, 0, 0, 354, 357, 1, 0, 0, 0, 355, 353, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 359, 1, 0, 0, 0, 357, 355, 1, 0, 0, 0, 358, 360, 3, 54, 27, 0, 359, 358, 1, 0, 0, 0, 359, 360, 1, 0, 0, 0, 360, 363, 1, 0, 0, 0, 361, 362, 5, 29, 0, 0, 362, 364, 3, 30, 15, 0, 363, 361, 1, 0, 0, 0, 363, 364, 1, 0, 0, 0, 364, 49, 1, 0, 0, 0, 365, 366, 5, 4, 0, 0, 366, 367, 3, 30, 15, 0, 367, 51, 1, 0, 0, 0, 368, 370, 5, 15, 0, 0, 369, 371, 3, 54, 27, 0, 370, 369, 1, 0, 0, 0, 370, 371, 1, 0, 0, 0, 371, 374, 1, 0, 0, 0, 372, 373, 5, 29, 0, 0, 373, 375, 3, 30, 15, 0, 374, 372, 1, 0, 0, 0, 374, 375, 1, 0, 0, 0, 375, 53, 1, 0, 0, 0, 376, 381, 3, 56, 28, 0, 377, 378, 5, 34, 0, 0, 378, 380, 3, 56, 28, 0, 379, 377, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 379, 1, 0, 0, 0, 381, 382, 1, 0, 0, 0, 382, 55, 1, 0, 0, 0, 383, 381, 1, 0, 0, 0, 384, 387, 3, 32, 16, 0, 385, 386, 5, 16, 0, 0, 386, 388, 3, 10, 5, 0, 387, 385, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 57, 1, 0, 0, 0, 389, 394, 3, 72, 36, 0, 390, 391, 5, 36, 0, 0, 391, 393, 3, 72, 36, 0, 392, 390, 1, 0, 0, 0, 393, 396, 1, 0, 0, 0, 394, 392, 1, 0, 0, 0, 394, 395, 1, 0, 0, 0, 395, 59, 1, 0, 0, 0, 396, 394, 1, 0, 0, 0, 397, 402, 3, 66, 33, 0, 398, 399, 5, 36, 0, 0, 399, 401, 3, 66, 33, 0, 400, 398, 1, 0, 0, 0, 401, 404, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 61, 1, 0, 0, 0, 404, 402, 1, 0, 0, 0, 405, 410, 3, 60, 30, 0, 406, 407, 5, 34, 0, 0, 407, 409, 3, 60, 30, 0, 408, 406, 1, 0, 0, 0, 409, 412, 1, 0, 0, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 63, 1, 0, 0, 0, 412, 410, 1, 0, 0, 0, 413, 414, 7, 3, 0, 0, 414, 65, 1, 0, 0, 0, 415, 419, 5, 80, 0, 0, 416, 417, 4, 33, 10, 0, 417, 419, 3, 70, 35, 0, 418, 415, 1, 0, 0, 0, 418, 416, 1, 0, 0, 0, 419, 67, 1, 0, 0, 0, 420, 463, 5, 45, 0, 0, 421, 422, 3, 104, 52, 0, 422, 423, 5, 67, 0, 0, 423, 463, 1, 0, 0, 0, 424, 463, 3, 102, 51, 0, 425, 463, 3, 104, 52, 0, 426, 463, 3, 98, 49, 0, 427, 463, 3, 70, 35, 0, 428, 463, 3, 106, 53, 0, 429, 430, 5, 65, 0, 0, 430, 435, 3, 100, 50, 0, 431, 432, 5, 34, 0, 0, 432, 434, 3, 100, 50, 0, 433, 431, 1, 0, 0, 0, 434, 437, 1, 0, 0, 0, 435, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 438, 1, 0, 0, 0, 437, 435, 1, 0, 0, 0, 438, 439, 5, 66, 0, 0, 439, 463, 1, 0, 0, 0, 440, 441, 5, 65, 0, 0, 441, 446, 3, 98, 49, 0, 442, 443, 5, 34, 0, 0, 443, 445, 3, 98, 49, 0, 444, 442, 1, 0, 0, 0, 445, 448, 1, 0, 0, 0, 446, 444, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 446, 1, 0, 0, 0, 449, 450, 5, 66, 0, 0, 450, 463, 1, 0, 0, 0, 451, 452, 5, 65, 0, 0, 452, 457, 3, 106, 53, 0, 453, 454, 5, 34, 0, 0, 454, 456, 3, 106, 53, 0, 455, 453, 1, 0, 0, 0, 456, 459, 1, 0, 0, 0, 457, 455, 1, 0, 0, 0, 457, 458, 1, 0, 0, 0, 458, 460, 1, 0, 0, 0, 459, 457, 1, 0, 0, 0, 460, 461, 5, 66, 0, 0, 461, 463, 1, 0, 0, 0, 462, 420, 1, 0, 0, 0, 462, 421, 1, 0, 0, 0, 462, 424, 1, 0, 0, 0, 462, 425, 1, 0, 0, 0, 462, 426, 1, 0, 0, 0, 462, 427, 1, 0, 0, 0, 462, 428, 1, 0, 0, 0, 462, 429, 1, 0, 0, 0, 462, 440, 1, 0, 0, 0, 462, 451, 1, 0, 0, 0, 463, 69, 1, 0, 0, 0, 464, 467, 5, 48, 0, 0, 465, 467, 5, 64, 0, 0, 466, 464, 1, 0, 0, 0, 466, 465, 1, 0, 0, 0, 467, 71, 1, 0, 0, 0, 468, 472, 3, 64, 32, 0, 469, 470, 4, 36, 11, 0, 470, 472, 3, 70, 35, 0, 471, 468, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 472, 73, 1, 0, 0, 0, 473, 474, 5, 9, 0, 0, 474, 475, 5, 27, 0, 0, 475, 75, 1, 0, 0, 0, 476, 477, 5, 14, 0, 0, 477, 482, 3, 78, 39, 0, 478, 479, 5, 34, 0, 0, 479, 481, 3, 78, 39, 0, 480, 478, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 480, 1, 0, 0, 0, 482, 483, 1, 0, 0, 0, 483, 77, 1, 0, 0, 0, 484, 482, 1, 0, 0, 0, 485, 487, 3, 10, 5, 0, 486, 488, 7, 4, 0, 0, 487, 486, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 491, 1, 0, 0, 0, 489, 490, 5, 46, 0, 0, 490, 492, 7, 5, 0, 0, 491, 489, 1, 0, 0, 0, 491, 492, 1, 0, 0, 0, 492, 79, 1, 0, 0, 0, 493, 494, 5, 8, 0, 0, 494, 495, 3, 62, 31, 0, 495, 81, 1, 0, 0, 0, 496, 497, 5, 2, 0, 0, 497, 498, 3, 62, 31, 0, 498, 83, 1, 0, 0, 0, 499, 500, 5, 11, 0, 0, 500, 505, 3, 86, 43, 0, 501, 502, 5, 34, 0, 0, 502, 504, 3, 86, 43, 0, 503, 501, 1, 0, 0, 0, 504, 507, 1, 0, 0, 0, 505, 503, 1, 0, 0, 0, 505, 506, 1, 0, 0, 0, 506, 85, 1, 0, 0, 0, 507, 505, 1, 0, 0, 0, 508, 509, 3, 60, 30, 0, 509, 510, 5, 84, 0, 0, 510, 511, 3, 60, 30, 0, 511, 87, 1, 0, 0, 0, 512, 513, 5, 1, 0, 0, 513, 514, 3, 20, 10, 0, 514, 516, 3, 106, 53, 0, 515, 517, 3, 94, 47, 0, 516, 515, 1, 0, 0, 0, 516, 517, 1, 0, 0, 0, 517, 89, 1, 0, 0, 0, 518, 519, 5, 7, 0, 0, 519, 520, 3, 20, 10, 0, 520, 521, 3, 106, 53, 0, 521, 91, 1, 0, 0, 0, 522, 523, 5, 10, 0, 0, 523, 524, 3, 58, 29, 0, 524, 93, 1, 0, 0, 0, 525, 530, 3, 96, 48, 0, 526, 527, 5, 34, 0, 0, 527, 529, 3, 96, 48, 0, 528, 526, 1, 0, 0, 0, 529, 532, 1, 0, 0, 0, 530, 528, 1, 0, 0, 0, 530, 531, 1, 0, 0, 0, 531, 95, 1, 0, 0, 0, 532, 530, 1, 0, 0, 0, 533, 534, 3, 64, 32, 0, 534, 535, 5, 32, 0, 0, 535, 536, 3, 68, 34, 0, 536, 97, 1, 0, 0, 0, 537, 538, 7, 6, 0, 0, 538, 99, 1, 0, 0, 0, 539, 542, 3, 102, 51, 0, 540, 542, 3, 104, 52, 0, 541, 539, 1, 0, 0, 0, 541, 540, 1, 0, 0, 0, 542, 101, 1, 0, 0, 0, 543, 545, 7, 0, 0, 0, 544, 543, 1, 0, 0, 0, 544, 545, 1, 0, 0, 0, 545, 546, 1, 0, 0, 0, 546, 547, 5, 28, 0, 0, 547, 103, 1, 0, 0, 0, 548, 550, 7, 0, 0, 0, 549, 548, 1, 0, 0, 0, 549, 550, 1, 0, 0, 0, 550, 551, 1, 0, 0, 0, 551, 552, 5, 27, 0, 0, 552, 105, 1, 0, 0, 0, 553, 554, 5, 26, 0, 0, 554, 107, 1, 0, 0, 0, 555, 556, 7, 7, 0, 0, 556, 109, 1, 0, 0, 0, 557, 558, 5, 5, 0, 0, 558, 559, 3, 112, 56, 0, 559, 111, 1, 0, 0, 0, 560, 561, 5, 65, 0, 0, 561, 562, 3, 2, 1, 0, 562, 563, 5, 66, 0, 0, 563, 113, 1, 0, 0, 0, 564, 565, 5, 13, 0, 0, 565, 566, 5, 100, 0, 0, 566, 115, 1, 0, 0, 0, 567, 568, 5, 3, 0, 0, 568, 571, 5, 90, 0, 0, 569, 570, 5, 88, 0, 0, 570, 572, 3, 60, 30, 0, 571, 569, 1, 0, 0, 0, 571, 572, 1, 0, 0, 0, 572, 582, 1, 0, 0, 0, 573, 574, 5, 89, 0, 0, 574, 579, 3, 118, 59, 0, 575, 576, 5, 34, 0, 0, 576, 578, 3, 118, 59, 0, 577, 575, 1, 0, 0, 0, 578, 581, 1, 0, 0, 0, 579, 577, 1, 0, 0, 0, 579, 580, 1, 0, 0, 0, 580, 583, 1, 0, 0, 0, 581, 579, 1, 0, 0, 0, 582, 573, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 117, 1, 0, 0, 0, 584, 585, 3, 60, 30, 0, 585, 586, 5, 32, 0, 0, 586, 588, 1, 0, 0, 0, 587, 584, 1, 0, 0, 0, 587, 588, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 590, 3, 60, 30, 0, 590, 119, 1, 0, 0, 0, 591, 592, 5, 18, 0, 0, 592, 593, 3, 36, 18, 0, 593, 594, 5, 88, 0, 0, 594, 595, 3, 62, 31, 0, 595, 121, 1, 0, 0, 0, 596, 597, 5, 17, 0, 0, 597, 600, 3, 54, 27, 0, 598, 599, 5, 29, 0, 0, 599, 601, 3, 30, 15, 0, 600, 598, 1, 0, 0, 0, 600, 601, 1, 0, 0, 0, 601, 123, 1, 0, 0, 0, 58, 135, 144, 162, 174, 183, 191, 197, 205, 207, 212, 219, 224, 235, 241, 249, 251, 262, 269, 280, 283, 299, 305, 315, 319, 324, 334, 342, 355, 359, 363, 370, 374, 381, 387, 394, 402, 410, 418, 435, 446, 457, 462, 466, 471, 482, 487, 491, 505, 516, 530, 541, 544, 549, 571, 579, 582, 587, 600] \ No newline at end of file +[4, 1, 128, 635, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 142, 8, 1, 10, 1, 12, 1, 145, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 153, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 173, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 185, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 192, 8, 5, 10, 5, 12, 5, 195, 9, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 202, 8, 5, 1, 5, 1, 5, 1, 5, 3, 5, 207, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 215, 8, 5, 10, 5, 12, 5, 218, 9, 5, 1, 6, 1, 6, 3, 6, 222, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 229, 8, 6, 1, 6, 1, 6, 1, 6, 3, 6, 234, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 245, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 251, 8, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 5, 9, 259, 8, 9, 10, 9, 12, 9, 262, 9, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 3, 10, 272, 8, 10, 1, 10, 1, 10, 1, 10, 5, 10, 277, 8, 10, 10, 10, 12, 10, 280, 9, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 5, 11, 288, 8, 11, 10, 11, 12, 11, 291, 9, 11, 3, 11, 293, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 307, 8, 15, 10, 15, 12, 15, 310, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 315, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 323, 8, 17, 10, 17, 12, 17, 326, 9, 17, 1, 17, 3, 17, 329, 8, 17, 1, 18, 1, 18, 1, 18, 3, 18, 334, 8, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 344, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 350, 8, 22, 10, 22, 12, 22, 353, 9, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 363, 8, 24, 10, 24, 12, 24, 366, 9, 24, 1, 24, 3, 24, 369, 8, 24, 1, 24, 1, 24, 3, 24, 373, 8, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 3, 26, 380, 8, 26, 1, 26, 1, 26, 3, 26, 384, 8, 26, 1, 27, 1, 27, 1, 27, 5, 27, 389, 8, 27, 10, 27, 12, 27, 392, 9, 27, 1, 28, 1, 28, 1, 28, 3, 28, 397, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 402, 8, 29, 10, 29, 12, 29, 405, 9, 29, 1, 30, 1, 30, 1, 30, 5, 30, 410, 8, 30, 10, 30, 12, 30, 413, 9, 30, 1, 31, 1, 31, 1, 31, 5, 31, 418, 8, 31, 10, 31, 12, 31, 421, 9, 31, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 3, 33, 428, 8, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 443, 8, 34, 10, 34, 12, 34, 446, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 454, 8, 34, 10, 34, 12, 34, 457, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 465, 8, 34, 10, 34, 12, 34, 468, 9, 34, 1, 34, 1, 34, 3, 34, 472, 8, 34, 1, 35, 1, 35, 3, 35, 476, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 481, 8, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 490, 8, 38, 10, 38, 12, 38, 493, 9, 38, 1, 39, 1, 39, 3, 39, 497, 8, 39, 1, 39, 1, 39, 3, 39, 501, 8, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 513, 8, 42, 10, 42, 12, 42, 516, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 526, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 5, 47, 538, 8, 47, 10, 47, 12, 47, 541, 9, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 3, 50, 551, 8, 50, 1, 51, 3, 51, 554, 8, 51, 1, 51, 1, 51, 1, 52, 3, 52, 559, 8, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 581, 8, 58, 1, 58, 1, 58, 1, 58, 1, 58, 5, 58, 587, 8, 58, 10, 58, 12, 58, 590, 9, 58, 3, 58, 592, 8, 58, 1, 59, 1, 59, 1, 59, 3, 59, 597, 8, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 610, 8, 61, 1, 62, 3, 62, 613, 8, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 3, 63, 622, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 5, 64, 628, 8, 64, 10, 64, 12, 64, 631, 9, 64, 1, 65, 1, 65, 1, 65, 0, 4, 2, 10, 18, 20, 66, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 0, 9, 1, 0, 64, 65, 1, 0, 66, 68, 2, 0, 30, 30, 81, 81, 1, 0, 72, 73, 2, 0, 35, 35, 40, 40, 2, 0, 43, 43, 46, 46, 2, 0, 42, 42, 56, 56, 2, 0, 57, 57, 59, 63, 1, 0, 22, 24, 660, 0, 132, 1, 0, 0, 0, 2, 135, 1, 0, 0, 0, 4, 152, 1, 0, 0, 0, 6, 172, 1, 0, 0, 0, 8, 174, 1, 0, 0, 0, 10, 206, 1, 0, 0, 0, 12, 233, 1, 0, 0, 0, 14, 235, 1, 0, 0, 0, 16, 244, 1, 0, 0, 0, 18, 250, 1, 0, 0, 0, 20, 271, 1, 0, 0, 0, 22, 281, 1, 0, 0, 0, 24, 296, 1, 0, 0, 0, 26, 298, 1, 0, 0, 0, 28, 300, 1, 0, 0, 0, 30, 303, 1, 0, 0, 0, 32, 314, 1, 0, 0, 0, 34, 318, 1, 0, 0, 0, 36, 333, 1, 0, 0, 0, 38, 337, 1, 0, 0, 0, 40, 339, 1, 0, 0, 0, 42, 343, 1, 0, 0, 0, 44, 345, 1, 0, 0, 0, 46, 354, 1, 0, 0, 0, 48, 358, 1, 0, 0, 0, 50, 374, 1, 0, 0, 0, 52, 377, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 393, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 406, 1, 0, 0, 0, 62, 414, 1, 0, 0, 0, 64, 422, 1, 0, 0, 0, 66, 427, 1, 0, 0, 0, 68, 471, 1, 0, 0, 0, 70, 475, 1, 0, 0, 0, 72, 480, 1, 0, 0, 0, 74, 482, 1, 0, 0, 0, 76, 485, 1, 0, 0, 0, 78, 494, 1, 0, 0, 0, 80, 502, 1, 0, 0, 0, 82, 505, 1, 0, 0, 0, 84, 508, 1, 0, 0, 0, 86, 517, 1, 0, 0, 0, 88, 521, 1, 0, 0, 0, 90, 527, 1, 0, 0, 0, 92, 531, 1, 0, 0, 0, 94, 534, 1, 0, 0, 0, 96, 542, 1, 0, 0, 0, 98, 546, 1, 0, 0, 0, 100, 550, 1, 0, 0, 0, 102, 553, 1, 0, 0, 0, 104, 558, 1, 0, 0, 0, 106, 562, 1, 0, 0, 0, 108, 564, 1, 0, 0, 0, 110, 566, 1, 0, 0, 0, 112, 569, 1, 0, 0, 0, 114, 573, 1, 0, 0, 0, 116, 576, 1, 0, 0, 0, 118, 596, 1, 0, 0, 0, 120, 600, 1, 0, 0, 0, 122, 605, 1, 0, 0, 0, 124, 612, 1, 0, 0, 0, 126, 618, 1, 0, 0, 0, 128, 623, 1, 0, 0, 0, 130, 632, 1, 0, 0, 0, 132, 133, 3, 2, 1, 0, 133, 134, 5, 0, 0, 1, 134, 1, 1, 0, 0, 0, 135, 136, 6, 1, -1, 0, 136, 137, 3, 4, 2, 0, 137, 143, 1, 0, 0, 0, 138, 139, 10, 1, 0, 0, 139, 140, 5, 29, 0, 0, 140, 142, 3, 6, 3, 0, 141, 138, 1, 0, 0, 0, 142, 145, 1, 0, 0, 0, 143, 141, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144, 3, 1, 0, 0, 0, 145, 143, 1, 0, 0, 0, 146, 153, 3, 110, 55, 0, 147, 153, 3, 34, 17, 0, 148, 153, 3, 28, 14, 0, 149, 153, 3, 114, 57, 0, 150, 151, 4, 2, 1, 0, 151, 153, 3, 48, 24, 0, 152, 146, 1, 0, 0, 0, 152, 147, 1, 0, 0, 0, 152, 148, 1, 0, 0, 0, 152, 149, 1, 0, 0, 0, 152, 150, 1, 0, 0, 0, 153, 5, 1, 0, 0, 0, 154, 173, 3, 50, 25, 0, 155, 173, 3, 8, 4, 0, 156, 173, 3, 80, 40, 0, 157, 173, 3, 74, 37, 0, 158, 173, 3, 52, 26, 0, 159, 173, 3, 76, 38, 0, 160, 173, 3, 82, 41, 0, 161, 173, 3, 84, 42, 0, 162, 173, 3, 88, 44, 0, 163, 173, 3, 90, 45, 0, 164, 173, 3, 116, 58, 0, 165, 173, 3, 92, 46, 0, 166, 167, 4, 3, 2, 0, 167, 173, 3, 122, 61, 0, 168, 169, 4, 3, 3, 0, 169, 173, 3, 120, 60, 0, 170, 171, 4, 3, 4, 0, 171, 173, 3, 124, 62, 0, 172, 154, 1, 0, 0, 0, 172, 155, 1, 0, 0, 0, 172, 156, 1, 0, 0, 0, 172, 157, 1, 0, 0, 0, 172, 158, 1, 0, 0, 0, 172, 159, 1, 0, 0, 0, 172, 160, 1, 0, 0, 0, 172, 161, 1, 0, 0, 0, 172, 162, 1, 0, 0, 0, 172, 163, 1, 0, 0, 0, 172, 164, 1, 0, 0, 0, 172, 165, 1, 0, 0, 0, 172, 166, 1, 0, 0, 0, 172, 168, 1, 0, 0, 0, 172, 170, 1, 0, 0, 0, 173, 7, 1, 0, 0, 0, 174, 175, 5, 16, 0, 0, 175, 176, 3, 10, 5, 0, 176, 9, 1, 0, 0, 0, 177, 178, 6, 5, -1, 0, 178, 179, 5, 49, 0, 0, 179, 207, 3, 10, 5, 8, 180, 207, 3, 16, 8, 0, 181, 207, 3, 12, 6, 0, 182, 184, 3, 16, 8, 0, 183, 185, 5, 49, 0, 0, 184, 183, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 5, 44, 0, 0, 187, 188, 5, 48, 0, 0, 188, 193, 3, 16, 8, 0, 189, 190, 5, 39, 0, 0, 190, 192, 3, 16, 8, 0, 191, 189, 1, 0, 0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 196, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 197, 5, 55, 0, 0, 197, 207, 1, 0, 0, 0, 198, 199, 3, 16, 8, 0, 199, 201, 5, 45, 0, 0, 200, 202, 5, 49, 0, 0, 201, 200, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202, 203, 1, 0, 0, 0, 203, 204, 5, 50, 0, 0, 204, 207, 1, 0, 0, 0, 205, 207, 3, 14, 7, 0, 206, 177, 1, 0, 0, 0, 206, 180, 1, 0, 0, 0, 206, 181, 1, 0, 0, 0, 206, 182, 1, 0, 0, 0, 206, 198, 1, 0, 0, 0, 206, 205, 1, 0, 0, 0, 207, 216, 1, 0, 0, 0, 208, 209, 10, 5, 0, 0, 209, 210, 5, 34, 0, 0, 210, 215, 3, 10, 5, 6, 211, 212, 10, 4, 0, 0, 212, 213, 5, 52, 0, 0, 213, 215, 3, 10, 5, 5, 214, 208, 1, 0, 0, 0, 214, 211, 1, 0, 0, 0, 215, 218, 1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 11, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 219, 221, 3, 16, 8, 0, 220, 222, 5, 49, 0, 0, 221, 220, 1, 0, 0, 0, 221, 222, 1, 0, 0, 0, 222, 223, 1, 0, 0, 0, 223, 224, 5, 47, 0, 0, 224, 225, 3, 106, 53, 0, 225, 234, 1, 0, 0, 0, 226, 228, 3, 16, 8, 0, 227, 229, 5, 49, 0, 0, 228, 227, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 230, 1, 0, 0, 0, 230, 231, 5, 54, 0, 0, 231, 232, 3, 106, 53, 0, 232, 234, 1, 0, 0, 0, 233, 219, 1, 0, 0, 0, 233, 226, 1, 0, 0, 0, 234, 13, 1, 0, 0, 0, 235, 236, 3, 58, 29, 0, 236, 237, 5, 38, 0, 0, 237, 238, 3, 68, 34, 0, 238, 15, 1, 0, 0, 0, 239, 245, 3, 18, 9, 0, 240, 241, 3, 18, 9, 0, 241, 242, 3, 108, 54, 0, 242, 243, 3, 18, 9, 0, 243, 245, 1, 0, 0, 0, 244, 239, 1, 0, 0, 0, 244, 240, 1, 0, 0, 0, 245, 17, 1, 0, 0, 0, 246, 247, 6, 9, -1, 0, 247, 251, 3, 20, 10, 0, 248, 249, 7, 0, 0, 0, 249, 251, 3, 18, 9, 3, 250, 246, 1, 0, 0, 0, 250, 248, 1, 0, 0, 0, 251, 260, 1, 0, 0, 0, 252, 253, 10, 2, 0, 0, 253, 254, 7, 1, 0, 0, 254, 259, 3, 18, 9, 3, 255, 256, 10, 1, 0, 0, 256, 257, 7, 0, 0, 0, 257, 259, 3, 18, 9, 2, 258, 252, 1, 0, 0, 0, 258, 255, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 19, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 264, 6, 10, -1, 0, 264, 272, 3, 68, 34, 0, 265, 272, 3, 58, 29, 0, 266, 272, 3, 22, 11, 0, 267, 268, 5, 48, 0, 0, 268, 269, 3, 10, 5, 0, 269, 270, 5, 55, 0, 0, 270, 272, 1, 0, 0, 0, 271, 263, 1, 0, 0, 0, 271, 265, 1, 0, 0, 0, 271, 266, 1, 0, 0, 0, 271, 267, 1, 0, 0, 0, 272, 278, 1, 0, 0, 0, 273, 274, 10, 1, 0, 0, 274, 275, 5, 37, 0, 0, 275, 277, 3, 26, 13, 0, 276, 273, 1, 0, 0, 0, 277, 280, 1, 0, 0, 0, 278, 276, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 21, 1, 0, 0, 0, 280, 278, 1, 0, 0, 0, 281, 282, 3, 24, 12, 0, 282, 292, 5, 48, 0, 0, 283, 293, 5, 66, 0, 0, 284, 289, 3, 10, 5, 0, 285, 286, 5, 39, 0, 0, 286, 288, 3, 10, 5, 0, 287, 285, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289, 290, 1, 0, 0, 0, 290, 293, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 283, 1, 0, 0, 0, 292, 284, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 5, 55, 0, 0, 295, 23, 1, 0, 0, 0, 296, 297, 3, 72, 36, 0, 297, 25, 1, 0, 0, 0, 298, 299, 3, 64, 32, 0, 299, 27, 1, 0, 0, 0, 300, 301, 5, 12, 0, 0, 301, 302, 3, 30, 15, 0, 302, 29, 1, 0, 0, 0, 303, 308, 3, 32, 16, 0, 304, 305, 5, 39, 0, 0, 305, 307, 3, 32, 16, 0, 306, 304, 1, 0, 0, 0, 307, 310, 1, 0, 0, 0, 308, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 31, 1, 0, 0, 0, 310, 308, 1, 0, 0, 0, 311, 312, 3, 58, 29, 0, 312, 313, 5, 36, 0, 0, 313, 315, 1, 0, 0, 0, 314, 311, 1, 0, 0, 0, 314, 315, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 317, 3, 10, 5, 0, 317, 33, 1, 0, 0, 0, 318, 319, 5, 6, 0, 0, 319, 324, 3, 36, 18, 0, 320, 321, 5, 39, 0, 0, 321, 323, 3, 36, 18, 0, 322, 320, 1, 0, 0, 0, 323, 326, 1, 0, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 328, 1, 0, 0, 0, 326, 324, 1, 0, 0, 0, 327, 329, 3, 42, 21, 0, 328, 327, 1, 0, 0, 0, 328, 329, 1, 0, 0, 0, 329, 35, 1, 0, 0, 0, 330, 331, 3, 38, 19, 0, 331, 332, 5, 38, 0, 0, 332, 334, 1, 0, 0, 0, 333, 330, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 335, 1, 0, 0, 0, 335, 336, 3, 40, 20, 0, 336, 37, 1, 0, 0, 0, 337, 338, 5, 81, 0, 0, 338, 39, 1, 0, 0, 0, 339, 340, 7, 2, 0, 0, 340, 41, 1, 0, 0, 0, 341, 344, 3, 44, 22, 0, 342, 344, 3, 46, 23, 0, 343, 341, 1, 0, 0, 0, 343, 342, 1, 0, 0, 0, 344, 43, 1, 0, 0, 0, 345, 346, 5, 80, 0, 0, 346, 351, 5, 81, 0, 0, 347, 348, 5, 39, 0, 0, 348, 350, 5, 81, 0, 0, 349, 347, 1, 0, 0, 0, 350, 353, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 45, 1, 0, 0, 0, 353, 351, 1, 0, 0, 0, 354, 355, 5, 70, 0, 0, 355, 356, 3, 44, 22, 0, 356, 357, 5, 71, 0, 0, 357, 47, 1, 0, 0, 0, 358, 359, 5, 19, 0, 0, 359, 364, 3, 36, 18, 0, 360, 361, 5, 39, 0, 0, 361, 363, 3, 36, 18, 0, 362, 360, 1, 0, 0, 0, 363, 366, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 364, 365, 1, 0, 0, 0, 365, 368, 1, 0, 0, 0, 366, 364, 1, 0, 0, 0, 367, 369, 3, 54, 27, 0, 368, 367, 1, 0, 0, 0, 368, 369, 1, 0, 0, 0, 369, 372, 1, 0, 0, 0, 370, 371, 5, 33, 0, 0, 371, 373, 3, 30, 15, 0, 372, 370, 1, 0, 0, 0, 372, 373, 1, 0, 0, 0, 373, 49, 1, 0, 0, 0, 374, 375, 5, 4, 0, 0, 375, 376, 3, 30, 15, 0, 376, 51, 1, 0, 0, 0, 377, 379, 5, 15, 0, 0, 378, 380, 3, 54, 27, 0, 379, 378, 1, 0, 0, 0, 379, 380, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 382, 5, 33, 0, 0, 382, 384, 3, 30, 15, 0, 383, 381, 1, 0, 0, 0, 383, 384, 1, 0, 0, 0, 384, 53, 1, 0, 0, 0, 385, 390, 3, 56, 28, 0, 386, 387, 5, 39, 0, 0, 387, 389, 3, 56, 28, 0, 388, 386, 1, 0, 0, 0, 389, 392, 1, 0, 0, 0, 390, 388, 1, 0, 0, 0, 390, 391, 1, 0, 0, 0, 391, 55, 1, 0, 0, 0, 392, 390, 1, 0, 0, 0, 393, 396, 3, 32, 16, 0, 394, 395, 5, 16, 0, 0, 395, 397, 3, 10, 5, 0, 396, 394, 1, 0, 0, 0, 396, 397, 1, 0, 0, 0, 397, 57, 1, 0, 0, 0, 398, 403, 3, 72, 36, 0, 399, 400, 5, 41, 0, 0, 400, 402, 3, 72, 36, 0, 401, 399, 1, 0, 0, 0, 402, 405, 1, 0, 0, 0, 403, 401, 1, 0, 0, 0, 403, 404, 1, 0, 0, 0, 404, 59, 1, 0, 0, 0, 405, 403, 1, 0, 0, 0, 406, 411, 3, 66, 33, 0, 407, 408, 5, 41, 0, 0, 408, 410, 3, 66, 33, 0, 409, 407, 1, 0, 0, 0, 410, 413, 1, 0, 0, 0, 411, 409, 1, 0, 0, 0, 411, 412, 1, 0, 0, 0, 412, 61, 1, 0, 0, 0, 413, 411, 1, 0, 0, 0, 414, 419, 3, 60, 30, 0, 415, 416, 5, 39, 0, 0, 416, 418, 3, 60, 30, 0, 417, 415, 1, 0, 0, 0, 418, 421, 1, 0, 0, 0, 419, 417, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 63, 1, 0, 0, 0, 421, 419, 1, 0, 0, 0, 422, 423, 7, 3, 0, 0, 423, 65, 1, 0, 0, 0, 424, 428, 5, 85, 0, 0, 425, 426, 4, 33, 10, 0, 426, 428, 3, 70, 35, 0, 427, 424, 1, 0, 0, 0, 427, 425, 1, 0, 0, 0, 428, 67, 1, 0, 0, 0, 429, 472, 5, 50, 0, 0, 430, 431, 3, 104, 52, 0, 431, 432, 5, 72, 0, 0, 432, 472, 1, 0, 0, 0, 433, 472, 3, 102, 51, 0, 434, 472, 3, 104, 52, 0, 435, 472, 3, 98, 49, 0, 436, 472, 3, 70, 35, 0, 437, 472, 3, 106, 53, 0, 438, 439, 5, 70, 0, 0, 439, 444, 3, 100, 50, 0, 440, 441, 5, 39, 0, 0, 441, 443, 3, 100, 50, 0, 442, 440, 1, 0, 0, 0, 443, 446, 1, 0, 0, 0, 444, 442, 1, 0, 0, 0, 444, 445, 1, 0, 0, 0, 445, 447, 1, 0, 0, 0, 446, 444, 1, 0, 0, 0, 447, 448, 5, 71, 0, 0, 448, 472, 1, 0, 0, 0, 449, 450, 5, 70, 0, 0, 450, 455, 3, 98, 49, 0, 451, 452, 5, 39, 0, 0, 452, 454, 3, 98, 49, 0, 453, 451, 1, 0, 0, 0, 454, 457, 1, 0, 0, 0, 455, 453, 1, 0, 0, 0, 455, 456, 1, 0, 0, 0, 456, 458, 1, 0, 0, 0, 457, 455, 1, 0, 0, 0, 458, 459, 5, 71, 0, 0, 459, 472, 1, 0, 0, 0, 460, 461, 5, 70, 0, 0, 461, 466, 3, 106, 53, 0, 462, 463, 5, 39, 0, 0, 463, 465, 3, 106, 53, 0, 464, 462, 1, 0, 0, 0, 465, 468, 1, 0, 0, 0, 466, 464, 1, 0, 0, 0, 466, 467, 1, 0, 0, 0, 467, 469, 1, 0, 0, 0, 468, 466, 1, 0, 0, 0, 469, 470, 5, 71, 0, 0, 470, 472, 1, 0, 0, 0, 471, 429, 1, 0, 0, 0, 471, 430, 1, 0, 0, 0, 471, 433, 1, 0, 0, 0, 471, 434, 1, 0, 0, 0, 471, 435, 1, 0, 0, 0, 471, 436, 1, 0, 0, 0, 471, 437, 1, 0, 0, 0, 471, 438, 1, 0, 0, 0, 471, 449, 1, 0, 0, 0, 471, 460, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 476, 5, 53, 0, 0, 474, 476, 5, 69, 0, 0, 475, 473, 1, 0, 0, 0, 475, 474, 1, 0, 0, 0, 476, 71, 1, 0, 0, 0, 477, 481, 3, 64, 32, 0, 478, 479, 4, 36, 11, 0, 479, 481, 3, 70, 35, 0, 480, 477, 1, 0, 0, 0, 480, 478, 1, 0, 0, 0, 481, 73, 1, 0, 0, 0, 482, 483, 5, 9, 0, 0, 483, 484, 5, 31, 0, 0, 484, 75, 1, 0, 0, 0, 485, 486, 5, 14, 0, 0, 486, 491, 3, 78, 39, 0, 487, 488, 5, 39, 0, 0, 488, 490, 3, 78, 39, 0, 489, 487, 1, 0, 0, 0, 490, 493, 1, 0, 0, 0, 491, 489, 1, 0, 0, 0, 491, 492, 1, 0, 0, 0, 492, 77, 1, 0, 0, 0, 493, 491, 1, 0, 0, 0, 494, 496, 3, 10, 5, 0, 495, 497, 7, 4, 0, 0, 496, 495, 1, 0, 0, 0, 496, 497, 1, 0, 0, 0, 497, 500, 1, 0, 0, 0, 498, 499, 5, 51, 0, 0, 499, 501, 7, 5, 0, 0, 500, 498, 1, 0, 0, 0, 500, 501, 1, 0, 0, 0, 501, 79, 1, 0, 0, 0, 502, 503, 5, 8, 0, 0, 503, 504, 3, 62, 31, 0, 504, 81, 1, 0, 0, 0, 505, 506, 5, 2, 0, 0, 506, 507, 3, 62, 31, 0, 507, 83, 1, 0, 0, 0, 508, 509, 5, 11, 0, 0, 509, 514, 3, 86, 43, 0, 510, 511, 5, 39, 0, 0, 511, 513, 3, 86, 43, 0, 512, 510, 1, 0, 0, 0, 513, 516, 1, 0, 0, 0, 514, 512, 1, 0, 0, 0, 514, 515, 1, 0, 0, 0, 515, 85, 1, 0, 0, 0, 516, 514, 1, 0, 0, 0, 517, 518, 3, 60, 30, 0, 518, 519, 5, 89, 0, 0, 519, 520, 3, 60, 30, 0, 520, 87, 1, 0, 0, 0, 521, 522, 5, 1, 0, 0, 522, 523, 3, 20, 10, 0, 523, 525, 3, 106, 53, 0, 524, 526, 3, 94, 47, 0, 525, 524, 1, 0, 0, 0, 525, 526, 1, 0, 0, 0, 526, 89, 1, 0, 0, 0, 527, 528, 5, 7, 0, 0, 528, 529, 3, 20, 10, 0, 529, 530, 3, 106, 53, 0, 530, 91, 1, 0, 0, 0, 531, 532, 5, 10, 0, 0, 532, 533, 3, 58, 29, 0, 533, 93, 1, 0, 0, 0, 534, 539, 3, 96, 48, 0, 535, 536, 5, 39, 0, 0, 536, 538, 3, 96, 48, 0, 537, 535, 1, 0, 0, 0, 538, 541, 1, 0, 0, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 95, 1, 0, 0, 0, 541, 539, 1, 0, 0, 0, 542, 543, 3, 64, 32, 0, 543, 544, 5, 36, 0, 0, 544, 545, 3, 68, 34, 0, 545, 97, 1, 0, 0, 0, 546, 547, 7, 6, 0, 0, 547, 99, 1, 0, 0, 0, 548, 551, 3, 102, 51, 0, 549, 551, 3, 104, 52, 0, 550, 548, 1, 0, 0, 0, 550, 549, 1, 0, 0, 0, 551, 101, 1, 0, 0, 0, 552, 554, 7, 0, 0, 0, 553, 552, 1, 0, 0, 0, 553, 554, 1, 0, 0, 0, 554, 555, 1, 0, 0, 0, 555, 556, 5, 32, 0, 0, 556, 103, 1, 0, 0, 0, 557, 559, 7, 0, 0, 0, 558, 557, 1, 0, 0, 0, 558, 559, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 561, 5, 31, 0, 0, 561, 105, 1, 0, 0, 0, 562, 563, 5, 30, 0, 0, 563, 107, 1, 0, 0, 0, 564, 565, 7, 7, 0, 0, 565, 109, 1, 0, 0, 0, 566, 567, 5, 5, 0, 0, 567, 568, 3, 112, 56, 0, 568, 111, 1, 0, 0, 0, 569, 570, 5, 70, 0, 0, 570, 571, 3, 2, 1, 0, 571, 572, 5, 71, 0, 0, 572, 113, 1, 0, 0, 0, 573, 574, 5, 13, 0, 0, 574, 575, 5, 105, 0, 0, 575, 115, 1, 0, 0, 0, 576, 577, 5, 3, 0, 0, 577, 580, 5, 95, 0, 0, 578, 579, 5, 93, 0, 0, 579, 581, 3, 60, 30, 0, 580, 578, 1, 0, 0, 0, 580, 581, 1, 0, 0, 0, 581, 591, 1, 0, 0, 0, 582, 583, 5, 94, 0, 0, 583, 588, 3, 118, 59, 0, 584, 585, 5, 39, 0, 0, 585, 587, 3, 118, 59, 0, 586, 584, 1, 0, 0, 0, 587, 590, 1, 0, 0, 0, 588, 586, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 592, 1, 0, 0, 0, 590, 588, 1, 0, 0, 0, 591, 582, 1, 0, 0, 0, 591, 592, 1, 0, 0, 0, 592, 117, 1, 0, 0, 0, 593, 594, 3, 60, 30, 0, 594, 595, 5, 36, 0, 0, 595, 597, 1, 0, 0, 0, 596, 593, 1, 0, 0, 0, 596, 597, 1, 0, 0, 0, 597, 598, 1, 0, 0, 0, 598, 599, 3, 60, 30, 0, 599, 119, 1, 0, 0, 0, 600, 601, 5, 18, 0, 0, 601, 602, 3, 36, 18, 0, 602, 603, 5, 93, 0, 0, 603, 604, 3, 62, 31, 0, 604, 121, 1, 0, 0, 0, 605, 606, 5, 17, 0, 0, 606, 609, 3, 54, 27, 0, 607, 608, 5, 33, 0, 0, 608, 610, 3, 30, 15, 0, 609, 607, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 610, 123, 1, 0, 0, 0, 611, 613, 7, 8, 0, 0, 612, 611, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 615, 5, 20, 0, 0, 615, 616, 3, 126, 63, 0, 616, 617, 3, 128, 64, 0, 617, 125, 1, 0, 0, 0, 618, 621, 3, 64, 32, 0, 619, 620, 5, 89, 0, 0, 620, 622, 3, 64, 32, 0, 621, 619, 1, 0, 0, 0, 621, 622, 1, 0, 0, 0, 622, 127, 1, 0, 0, 0, 623, 624, 5, 93, 0, 0, 624, 629, 3, 130, 65, 0, 625, 626, 5, 39, 0, 0, 626, 628, 3, 130, 65, 0, 627, 625, 1, 0, 0, 0, 628, 631, 1, 0, 0, 0, 629, 627, 1, 0, 0, 0, 629, 630, 1, 0, 0, 0, 630, 129, 1, 0, 0, 0, 631, 629, 1, 0, 0, 0, 632, 633, 3, 16, 8, 0, 633, 131, 1, 0, 0, 0, 61, 143, 152, 172, 184, 193, 201, 206, 214, 216, 221, 228, 233, 244, 250, 258, 260, 271, 278, 289, 292, 308, 314, 324, 328, 333, 343, 351, 364, 368, 372, 379, 383, 390, 396, 403, 411, 419, 427, 444, 455, 466, 471, 475, 480, 491, 496, 500, 514, 525, 539, 550, 553, 558, 580, 588, 591, 596, 609, 612, 621, 629] \ No newline at end of file diff --git a/packages/kbn-esql-ast/src/antlr/esql_parser.tokens b/packages/kbn-esql-ast/src/antlr/esql_parser.tokens index 3dd1a2c754038..b1a16987dd8ce 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_parser.tokens +++ b/packages/kbn-esql-ast/src/antlr/esql_parser.tokens @@ -17,106 +17,115 @@ WHERE=16 DEV_INLINESTATS=17 DEV_LOOKUP=18 DEV_METRICS=19 -UNKNOWN_CMD=20 -LINE_COMMENT=21 -MULTILINE_COMMENT=22 -WS=23 -COLON=24 -PIPE=25 -QUOTED_STRING=26 -INTEGER_LITERAL=27 -DECIMAL_LITERAL=28 -BY=29 -AND=30 -ASC=31 -ASSIGN=32 -CAST_OP=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -IN=39 -IS=40 -LAST=41 -LIKE=42 -LP=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -EQ=52 -CIEQ=53 -NEQ=54 -LT=55 -LTE=56 -GT=57 -GTE=58 -PLUS=59 -MINUS=60 -ASTERISK=61 -SLASH=62 -PERCENT=63 -NAMED_OR_POSITIONAL_PARAM=64 -OPENING_BRACKET=65 -CLOSING_BRACKET=66 -UNQUOTED_IDENTIFIER=67 -QUOTED_IDENTIFIER=68 -EXPR_LINE_COMMENT=69 -EXPR_MULTILINE_COMMENT=70 -EXPR_WS=71 -EXPLAIN_WS=72 -EXPLAIN_LINE_COMMENT=73 -EXPLAIN_MULTILINE_COMMENT=74 -METADATA=75 -UNQUOTED_SOURCE=76 -FROM_LINE_COMMENT=77 -FROM_MULTILINE_COMMENT=78 -FROM_WS=79 -ID_PATTERN=80 -PROJECT_LINE_COMMENT=81 -PROJECT_MULTILINE_COMMENT=82 -PROJECT_WS=83 -AS=84 -RENAME_LINE_COMMENT=85 -RENAME_MULTILINE_COMMENT=86 -RENAME_WS=87 -ON=88 -WITH=89 -ENRICH_POLICY_NAME=90 -ENRICH_LINE_COMMENT=91 -ENRICH_MULTILINE_COMMENT=92 -ENRICH_WS=93 -ENRICH_FIELD_LINE_COMMENT=94 -ENRICH_FIELD_MULTILINE_COMMENT=95 -ENRICH_FIELD_WS=96 -MVEXPAND_LINE_COMMENT=97 -MVEXPAND_MULTILINE_COMMENT=98 -MVEXPAND_WS=99 -INFO=100 -SHOW_LINE_COMMENT=101 -SHOW_MULTILINE_COMMENT=102 -SHOW_WS=103 -SETTING=104 -SETTING_LINE_COMMENT=105 -SETTTING_MULTILINE_COMMENT=106 -SETTING_WS=107 -LOOKUP_LINE_COMMENT=108 -LOOKUP_MULTILINE_COMMENT=109 -LOOKUP_WS=110 -LOOKUP_FIELD_LINE_COMMENT=111 -LOOKUP_FIELD_MULTILINE_COMMENT=112 -LOOKUP_FIELD_WS=113 -METRICS_LINE_COMMENT=114 -METRICS_MULTILINE_COMMENT=115 -METRICS_WS=116 -CLOSING_METRICS_LINE_COMMENT=117 -CLOSING_METRICS_MULTILINE_COMMENT=118 -CLOSING_METRICS_WS=119 +DEV_JOIN=20 +DEV_JOIN_FULL=21 +DEV_JOIN_LEFT=22 +DEV_JOIN_RIGHT=23 +DEV_JOIN_LOOKUP=24 +UNKNOWN_CMD=25 +LINE_COMMENT=26 +MULTILINE_COMMENT=27 +WS=28 +PIPE=29 +QUOTED_STRING=30 +INTEGER_LITERAL=31 +DECIMAL_LITERAL=32 +BY=33 +AND=34 +ASC=35 +ASSIGN=36 +CAST_OP=37 +COLON=38 +COMMA=39 +DESC=40 +DOT=41 +FALSE=42 +FIRST=43 +IN=44 +IS=45 +LAST=46 +LIKE=47 +LP=48 +NOT=49 +NULL=50 +NULLS=51 +OR=52 +PARAM=53 +RLIKE=54 +RP=55 +TRUE=56 +EQ=57 +CIEQ=58 +NEQ=59 +LT=60 +LTE=61 +GT=62 +GTE=63 +PLUS=64 +MINUS=65 +ASTERISK=66 +SLASH=67 +PERCENT=68 +NAMED_OR_POSITIONAL_PARAM=69 +OPENING_BRACKET=70 +CLOSING_BRACKET=71 +UNQUOTED_IDENTIFIER=72 +QUOTED_IDENTIFIER=73 +EXPR_LINE_COMMENT=74 +EXPR_MULTILINE_COMMENT=75 +EXPR_WS=76 +EXPLAIN_WS=77 +EXPLAIN_LINE_COMMENT=78 +EXPLAIN_MULTILINE_COMMENT=79 +METADATA=80 +UNQUOTED_SOURCE=81 +FROM_LINE_COMMENT=82 +FROM_MULTILINE_COMMENT=83 +FROM_WS=84 +ID_PATTERN=85 +PROJECT_LINE_COMMENT=86 +PROJECT_MULTILINE_COMMENT=87 +PROJECT_WS=88 +AS=89 +RENAME_LINE_COMMENT=90 +RENAME_MULTILINE_COMMENT=91 +RENAME_WS=92 +ON=93 +WITH=94 +ENRICH_POLICY_NAME=95 +ENRICH_LINE_COMMENT=96 +ENRICH_MULTILINE_COMMENT=97 +ENRICH_WS=98 +ENRICH_FIELD_LINE_COMMENT=99 +ENRICH_FIELD_MULTILINE_COMMENT=100 +ENRICH_FIELD_WS=101 +MVEXPAND_LINE_COMMENT=102 +MVEXPAND_MULTILINE_COMMENT=103 +MVEXPAND_WS=104 +INFO=105 +SHOW_LINE_COMMENT=106 +SHOW_MULTILINE_COMMENT=107 +SHOW_WS=108 +SETTING=109 +SETTING_LINE_COMMENT=110 +SETTTING_MULTILINE_COMMENT=111 +SETTING_WS=112 +LOOKUP_LINE_COMMENT=113 +LOOKUP_MULTILINE_COMMENT=114 +LOOKUP_WS=115 +LOOKUP_FIELD_LINE_COMMENT=116 +LOOKUP_FIELD_MULTILINE_COMMENT=117 +LOOKUP_FIELD_WS=118 +USING=119 +JOIN_LINE_COMMENT=120 +JOIN_MULTILINE_COMMENT=121 +JOIN_WS=122 +METRICS_LINE_COMMENT=123 +METRICS_MULTILINE_COMMENT=124 +METRICS_WS=125 +CLOSING_METRICS_LINE_COMMENT=126 +CLOSING_METRICS_MULTILINE_COMMENT=127 +CLOSING_METRICS_WS=128 'dissect'=1 'drop'=2 'enrich'=3 @@ -133,46 +142,47 @@ CLOSING_METRICS_WS=119 'sort'=14 'stats'=15 'where'=16 -':'=24 -'|'=25 -'by'=29 -'and'=30 -'asc'=31 -'='=32 -'::'=33 -','=34 -'desc'=35 -'.'=36 -'false'=37 -'first'=38 -'in'=39 -'is'=40 -'last'=41 -'like'=42 -'('=43 -'not'=44 -'null'=45 -'nulls'=46 -'or'=47 -'?'=48 -'rlike'=49 -')'=50 -'true'=51 -'=='=52 -'=~'=53 -'!='=54 -'<'=55 -'<='=56 -'>'=57 -'>='=58 -'+'=59 -'-'=60 -'*'=61 -'/'=62 -'%'=63 -']'=66 -'metadata'=75 -'as'=84 -'on'=88 -'with'=89 -'info'=100 +'|'=29 +'by'=33 +'and'=34 +'asc'=35 +'='=36 +'::'=37 +':'=38 +','=39 +'desc'=40 +'.'=41 +'false'=42 +'first'=43 +'in'=44 +'is'=45 +'last'=46 +'like'=47 +'('=48 +'not'=49 +'null'=50 +'nulls'=51 +'or'=52 +'?'=53 +'rlike'=54 +')'=55 +'true'=56 +'=='=57 +'=~'=58 +'!='=59 +'<'=60 +'<='=61 +'>'=62 +'>='=63 +'+'=64 +'-'=65 +'*'=66 +'/'=67 +'%'=68 +']'=71 +'metadata'=80 +'as'=89 +'on'=93 +'with'=94 +'info'=105 +'USING'=119 diff --git a/packages/kbn-esql-ast/src/antlr/esql_parser.ts b/packages/kbn-esql-ast/src/antlr/esql_parser.ts index 4dc0c5c628e37..ec261299493a5 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_parser.ts +++ b/packages/kbn-esql-ast/src/antlr/esql_parser.ts @@ -47,106 +47,115 @@ export default class esql_parser extends parser_config { public static readonly DEV_INLINESTATS = 17; public static readonly DEV_LOOKUP = 18; public static readonly DEV_METRICS = 19; - public static readonly UNKNOWN_CMD = 20; - public static readonly LINE_COMMENT = 21; - public static readonly MULTILINE_COMMENT = 22; - public static readonly WS = 23; - public static readonly COLON = 24; - public static readonly PIPE = 25; - public static readonly QUOTED_STRING = 26; - public static readonly INTEGER_LITERAL = 27; - public static readonly DECIMAL_LITERAL = 28; - public static readonly BY = 29; - public static readonly AND = 30; - public static readonly ASC = 31; - public static readonly ASSIGN = 32; - public static readonly CAST_OP = 33; - public static readonly COMMA = 34; - public static readonly DESC = 35; - public static readonly DOT = 36; - public static readonly FALSE = 37; - public static readonly FIRST = 38; - public static readonly IN = 39; - public static readonly IS = 40; - public static readonly LAST = 41; - public static readonly LIKE = 42; - public static readonly LP = 43; - public static readonly NOT = 44; - public static readonly NULL = 45; - public static readonly NULLS = 46; - public static readonly OR = 47; - public static readonly PARAM = 48; - public static readonly RLIKE = 49; - public static readonly RP = 50; - public static readonly TRUE = 51; - public static readonly EQ = 52; - public static readonly CIEQ = 53; - public static readonly NEQ = 54; - public static readonly LT = 55; - public static readonly LTE = 56; - public static readonly GT = 57; - public static readonly GTE = 58; - public static readonly PLUS = 59; - public static readonly MINUS = 60; - public static readonly ASTERISK = 61; - public static readonly SLASH = 62; - public static readonly PERCENT = 63; - public static readonly NAMED_OR_POSITIONAL_PARAM = 64; - public static readonly OPENING_BRACKET = 65; - public static readonly CLOSING_BRACKET = 66; - public static readonly UNQUOTED_IDENTIFIER = 67; - public static readonly QUOTED_IDENTIFIER = 68; - public static readonly EXPR_LINE_COMMENT = 69; - public static readonly EXPR_MULTILINE_COMMENT = 70; - public static readonly EXPR_WS = 71; - public static readonly EXPLAIN_WS = 72; - public static readonly EXPLAIN_LINE_COMMENT = 73; - public static readonly EXPLAIN_MULTILINE_COMMENT = 74; - public static readonly METADATA = 75; - public static readonly UNQUOTED_SOURCE = 76; - public static readonly FROM_LINE_COMMENT = 77; - public static readonly FROM_MULTILINE_COMMENT = 78; - public static readonly FROM_WS = 79; - public static readonly ID_PATTERN = 80; - public static readonly PROJECT_LINE_COMMENT = 81; - public static readonly PROJECT_MULTILINE_COMMENT = 82; - public static readonly PROJECT_WS = 83; - public static readonly AS = 84; - public static readonly RENAME_LINE_COMMENT = 85; - public static readonly RENAME_MULTILINE_COMMENT = 86; - public static readonly RENAME_WS = 87; - public static readonly ON = 88; - public static readonly WITH = 89; - public static readonly ENRICH_POLICY_NAME = 90; - public static readonly ENRICH_LINE_COMMENT = 91; - public static readonly ENRICH_MULTILINE_COMMENT = 92; - public static readonly ENRICH_WS = 93; - public static readonly ENRICH_FIELD_LINE_COMMENT = 94; - public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 95; - public static readonly ENRICH_FIELD_WS = 96; - public static readonly MVEXPAND_LINE_COMMENT = 97; - public static readonly MVEXPAND_MULTILINE_COMMENT = 98; - public static readonly MVEXPAND_WS = 99; - public static readonly INFO = 100; - public static readonly SHOW_LINE_COMMENT = 101; - public static readonly SHOW_MULTILINE_COMMENT = 102; - public static readonly SHOW_WS = 103; - public static readonly SETTING = 104; - public static readonly SETTING_LINE_COMMENT = 105; - public static readonly SETTTING_MULTILINE_COMMENT = 106; - public static readonly SETTING_WS = 107; - public static readonly LOOKUP_LINE_COMMENT = 108; - public static readonly LOOKUP_MULTILINE_COMMENT = 109; - public static readonly LOOKUP_WS = 110; - public static readonly LOOKUP_FIELD_LINE_COMMENT = 111; - public static readonly LOOKUP_FIELD_MULTILINE_COMMENT = 112; - public static readonly LOOKUP_FIELD_WS = 113; - public static readonly METRICS_LINE_COMMENT = 114; - public static readonly METRICS_MULTILINE_COMMENT = 115; - public static readonly METRICS_WS = 116; - public static readonly CLOSING_METRICS_LINE_COMMENT = 117; - public static readonly CLOSING_METRICS_MULTILINE_COMMENT = 118; - public static readonly CLOSING_METRICS_WS = 119; + public static readonly DEV_JOIN = 20; + public static readonly DEV_JOIN_FULL = 21; + public static readonly DEV_JOIN_LEFT = 22; + public static readonly DEV_JOIN_RIGHT = 23; + public static readonly DEV_JOIN_LOOKUP = 24; + public static readonly UNKNOWN_CMD = 25; + public static readonly LINE_COMMENT = 26; + public static readonly MULTILINE_COMMENT = 27; + public static readonly WS = 28; + public static readonly PIPE = 29; + public static readonly QUOTED_STRING = 30; + public static readonly INTEGER_LITERAL = 31; + public static readonly DECIMAL_LITERAL = 32; + public static readonly BY = 33; + public static readonly AND = 34; + public static readonly ASC = 35; + public static readonly ASSIGN = 36; + public static readonly CAST_OP = 37; + public static readonly COLON = 38; + public static readonly COMMA = 39; + public static readonly DESC = 40; + public static readonly DOT = 41; + public static readonly FALSE = 42; + public static readonly FIRST = 43; + public static readonly IN = 44; + public static readonly IS = 45; + public static readonly LAST = 46; + public static readonly LIKE = 47; + public static readonly LP = 48; + public static readonly NOT = 49; + public static readonly NULL = 50; + public static readonly NULLS = 51; + public static readonly OR = 52; + public static readonly PARAM = 53; + public static readonly RLIKE = 54; + public static readonly RP = 55; + public static readonly TRUE = 56; + public static readonly EQ = 57; + public static readonly CIEQ = 58; + public static readonly NEQ = 59; + public static readonly LT = 60; + public static readonly LTE = 61; + public static readonly GT = 62; + public static readonly GTE = 63; + public static readonly PLUS = 64; + public static readonly MINUS = 65; + public static readonly ASTERISK = 66; + public static readonly SLASH = 67; + public static readonly PERCENT = 68; + public static readonly NAMED_OR_POSITIONAL_PARAM = 69; + public static readonly OPENING_BRACKET = 70; + public static readonly CLOSING_BRACKET = 71; + public static readonly UNQUOTED_IDENTIFIER = 72; + public static readonly QUOTED_IDENTIFIER = 73; + public static readonly EXPR_LINE_COMMENT = 74; + public static readonly EXPR_MULTILINE_COMMENT = 75; + public static readonly EXPR_WS = 76; + public static readonly EXPLAIN_WS = 77; + public static readonly EXPLAIN_LINE_COMMENT = 78; + public static readonly EXPLAIN_MULTILINE_COMMENT = 79; + public static readonly METADATA = 80; + public static readonly UNQUOTED_SOURCE = 81; + public static readonly FROM_LINE_COMMENT = 82; + public static readonly FROM_MULTILINE_COMMENT = 83; + public static readonly FROM_WS = 84; + public static readonly ID_PATTERN = 85; + public static readonly PROJECT_LINE_COMMENT = 86; + public static readonly PROJECT_MULTILINE_COMMENT = 87; + public static readonly PROJECT_WS = 88; + public static readonly AS = 89; + public static readonly RENAME_LINE_COMMENT = 90; + public static readonly RENAME_MULTILINE_COMMENT = 91; + public static readonly RENAME_WS = 92; + public static readonly ON = 93; + public static readonly WITH = 94; + public static readonly ENRICH_POLICY_NAME = 95; + public static readonly ENRICH_LINE_COMMENT = 96; + public static readonly ENRICH_MULTILINE_COMMENT = 97; + public static readonly ENRICH_WS = 98; + public static readonly ENRICH_FIELD_LINE_COMMENT = 99; + public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 100; + public static readonly ENRICH_FIELD_WS = 101; + public static readonly MVEXPAND_LINE_COMMENT = 102; + public static readonly MVEXPAND_MULTILINE_COMMENT = 103; + public static readonly MVEXPAND_WS = 104; + public static readonly INFO = 105; + public static readonly SHOW_LINE_COMMENT = 106; + public static readonly SHOW_MULTILINE_COMMENT = 107; + public static readonly SHOW_WS = 108; + public static readonly SETTING = 109; + public static readonly SETTING_LINE_COMMENT = 110; + public static readonly SETTTING_MULTILINE_COMMENT = 111; + public static readonly SETTING_WS = 112; + public static readonly LOOKUP_LINE_COMMENT = 113; + public static readonly LOOKUP_MULTILINE_COMMENT = 114; + public static readonly LOOKUP_WS = 115; + public static readonly LOOKUP_FIELD_LINE_COMMENT = 116; + public static readonly LOOKUP_FIELD_MULTILINE_COMMENT = 117; + public static readonly LOOKUP_FIELD_WS = 118; + public static readonly USING = 119; + public static readonly JOIN_LINE_COMMENT = 120; + public static readonly JOIN_MULTILINE_COMMENT = 121; + public static readonly JOIN_WS = 122; + public static readonly METRICS_LINE_COMMENT = 123; + public static readonly METRICS_MULTILINE_COMMENT = 124; + public static readonly METRICS_WS = 125; + public static readonly CLOSING_METRICS_LINE_COMMENT = 126; + public static readonly CLOSING_METRICS_MULTILINE_COMMENT = 127; + public static readonly CLOSING_METRICS_WS = 128; public static override readonly EOF = Token.EOF; public static readonly RULE_singleStatement = 0; public static readonly RULE_query = 1; @@ -210,6 +219,10 @@ export default class esql_parser extends parser_config { public static readonly RULE_enrichWithClause = 59; public static readonly RULE_lookupCommand = 60; public static readonly RULE_inlinestatsCommand = 61; + public static readonly RULE_joinCommand = 62; + public static readonly RULE_joinTarget = 63; + public static readonly RULE_joinCondition = 64; + public static readonly RULE_joinPredicate = 65; public static readonly literalNames: (string | null)[] = [ null, "'dissect'", "'drop'", "'enrich'", "'eval'", "'explain'", @@ -223,32 +236,35 @@ export default class esql_parser extends parser_config { null, null, null, null, null, null, - "':'", "'|'", + null, null, + null, null, + null, "'|'", null, null, null, "'by'", "'and'", "'asc'", "'='", "'::'", - "','", "'desc'", - "'.'", "'false'", - "'first'", "'in'", - "'is'", "'last'", - "'like'", "'('", - "'not'", "'null'", - "'nulls'", "'or'", - "'?'", "'rlike'", - "')'", "'true'", - "'=='", "'=~'", - "'!='", "'<'", - "'<='", "'>'", - "'>='", "'+'", - "'-'", "'*'", - "'/'", "'%'", + "':'", "','", + "'desc'", "'.'", + "'false'", "'first'", + "'in'", "'is'", + "'last'", "'like'", + "'('", "'not'", + "'null'", "'nulls'", + "'or'", "'?'", + "'rlike'", "')'", + "'true'", "'=='", + "'=~'", "'!='", + "'<'", "'<='", + "'>'", "'>='", + "'+'", "'-'", + "'*'", "'/'", + "'%'", null, + null, "']'", null, null, - "']'", null, null, null, null, null, null, null, - null, "'metadata'", + "'metadata'", null, null, null, null, null, null, @@ -261,7 +277,14 @@ export default class esql_parser extends parser_config { null, null, null, null, null, null, - "'info'" ]; + "'info'", null, + null, null, + null, null, + null, null, + null, null, + null, null, + null, null, + "'USING'" ]; public static readonly symbolicNames: (string | null)[] = [ null, "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", @@ -274,30 +297,36 @@ export default class esql_parser extends parser_config { "DEV_INLINESTATS", "DEV_LOOKUP", "DEV_METRICS", + "DEV_JOIN", + "DEV_JOIN_FULL", + "DEV_JOIN_LEFT", + "DEV_JOIN_RIGHT", + "DEV_JOIN_LOOKUP", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", - "WS", "COLON", - "PIPE", "QUOTED_STRING", + "WS", "PIPE", + "QUOTED_STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "CAST_OP", - "COMMA", "DESC", - "DOT", "FALSE", - "FIRST", "IN", - "IS", "LAST", - "LIKE", "LP", - "NOT", "NULL", - "NULLS", "OR", - "PARAM", "RLIKE", - "RP", "TRUE", - "EQ", "CIEQ", - "NEQ", "LT", - "LTE", "GT", - "GTE", "PLUS", - "MINUS", "ASTERISK", + "COLON", "COMMA", + "DESC", "DOT", + "FALSE", "FIRST", + "IN", "IS", + "LAST", "LIKE", + "LP", "NOT", + "NULL", "NULLS", + "OR", "PARAM", + "RLIKE", "RP", + "TRUE", "EQ", + "CIEQ", "NEQ", + "LT", "LTE", + "GT", "GTE", + "PLUS", "MINUS", + "ASTERISK", "SLASH", "PERCENT", "NAMED_OR_POSITIONAL_PARAM", "OPENING_BRACKET", @@ -346,6 +375,9 @@ export default class esql_parser extends parser_config { "LOOKUP_FIELD_LINE_COMMENT", "LOOKUP_FIELD_MULTILINE_COMMENT", "LOOKUP_FIELD_WS", + "USING", "JOIN_LINE_COMMENT", + "JOIN_MULTILINE_COMMENT", + "JOIN_WS", "METRICS_LINE_COMMENT", "METRICS_MULTILINE_COMMENT", "METRICS_WS", @@ -366,7 +398,8 @@ export default class esql_parser extends parser_config { "renameCommand", "renameClause", "dissectCommand", "grokCommand", "mvExpandCommand", "commandOptions", "commandOption", "booleanValue", "numericValue", "decimalValue", "integerValue", "string", "comparisonOperator", "explainCommand", "subqueryExpression", - "showCommand", "enrichCommand", "enrichWithClause", "lookupCommand", "inlinestatsCommand", + "showCommand", "enrichCommand", "enrichWithClause", "lookupCommand", "inlinestatsCommand", + "joinCommand", "joinTarget", "joinCondition", "joinPredicate", ]; public get grammarFileName(): string { return "esql_parser.g4"; } public get literalNames(): (string | null)[] { return esql_parser.literalNames; } @@ -389,9 +422,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 124; + this.state = 132; this.query(0); - this.state = 125; + this.state = 133; this.match(esql_parser.EOF); } } @@ -433,11 +466,11 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 128; + this.state = 136; this.sourceCommand(); } this._ctx.stop = this._input.LT(-1); - this.state = 135; + this.state = 143; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -450,18 +483,18 @@ export default class esql_parser extends parser_config { { localctx = new CompositeQueryContext(this, new QueryContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_query); - this.state = 130; + this.state = 138; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 131; + this.state = 139; this.match(esql_parser.PIPE); - this.state = 132; + this.state = 140; this.processingCommand(); } } } - this.state = 137; + this.state = 145; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); } @@ -486,45 +519,45 @@ export default class esql_parser extends parser_config { let localctx: SourceCommandContext = new SourceCommandContext(this, this._ctx, this.state); this.enterRule(localctx, 4, esql_parser.RULE_sourceCommand); try { - this.state = 144; + this.state = 152; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 1, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 138; + this.state = 146; this.explainCommand(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 139; + this.state = 147; this.fromCommand(); } break; case 3: this.enterOuterAlt(localctx, 3); { - this.state = 140; + this.state = 148; this.rowCommand(); } break; case 4: this.enterOuterAlt(localctx, 4); { - this.state = 141; + this.state = 149; this.showCommand(); } break; case 5: this.enterOuterAlt(localctx, 5); { - this.state = 142; + this.state = 150; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 143; + this.state = 151; this.metricsCommand(); } break; @@ -549,115 +582,126 @@ export default class esql_parser extends parser_config { let localctx: ProcessingCommandContext = new ProcessingCommandContext(this, this._ctx, this.state); this.enterRule(localctx, 6, esql_parser.RULE_processingCommand); try { - this.state = 162; + this.state = 172; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 2, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 146; + this.state = 154; this.evalCommand(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 147; + this.state = 155; this.whereCommand(); } break; case 3: this.enterOuterAlt(localctx, 3); { - this.state = 148; + this.state = 156; this.keepCommand(); } break; case 4: this.enterOuterAlt(localctx, 4); { - this.state = 149; + this.state = 157; this.limitCommand(); } break; case 5: this.enterOuterAlt(localctx, 5); { - this.state = 150; + this.state = 158; this.statsCommand(); } break; case 6: this.enterOuterAlt(localctx, 6); { - this.state = 151; + this.state = 159; this.sortCommand(); } break; case 7: this.enterOuterAlt(localctx, 7); { - this.state = 152; + this.state = 160; this.dropCommand(); } break; case 8: this.enterOuterAlt(localctx, 8); { - this.state = 153; + this.state = 161; this.renameCommand(); } break; case 9: this.enterOuterAlt(localctx, 9); { - this.state = 154; + this.state = 162; this.dissectCommand(); } break; case 10: this.enterOuterAlt(localctx, 10); { - this.state = 155; + this.state = 163; this.grokCommand(); } break; case 11: this.enterOuterAlt(localctx, 11); { - this.state = 156; + this.state = 164; this.enrichCommand(); } break; case 12: this.enterOuterAlt(localctx, 12); { - this.state = 157; + this.state = 165; this.mvExpandCommand(); } break; case 13: this.enterOuterAlt(localctx, 13); { - this.state = 158; + this.state = 166; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 159; + this.state = 167; this.inlinestatsCommand(); } break; case 14: this.enterOuterAlt(localctx, 14); { - this.state = 160; + this.state = 168; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 161; + this.state = 169; this.lookupCommand(); } break; + case 15: + this.enterOuterAlt(localctx, 15); + { + this.state = 170; + if (!(this.isDevVersion())) { + throw this.createFailedPredicateException("this.isDevVersion()"); + } + this.state = 171; + this.joinCommand(); + } + break; } } catch (re) { @@ -681,9 +725,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 164; + this.state = 174; this.match(esql_parser.WHERE); - this.state = 165; + this.state = 175; this.booleanExpression(0); } } @@ -721,7 +765,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 197; + this.state = 206; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 6, this._ctx) ) { case 1: @@ -730,9 +774,9 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 168; + this.state = 178; this.match(esql_parser.NOT); - this.state = 169; + this.state = 179; this.booleanExpression(8); } break; @@ -741,7 +785,7 @@ export default class esql_parser extends parser_config { localctx = new BooleanDefaultContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 170; + this.state = 180; this.valueExpression(); } break; @@ -750,7 +794,7 @@ export default class esql_parser extends parser_config { localctx = new RegexExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 171; + this.state = 181; this.regexBooleanExpression(); } break; @@ -759,41 +803,41 @@ export default class esql_parser extends parser_config { localctx = new LogicalInContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 172; + this.state = 182; this.valueExpression(); - this.state = 174; + this.state = 184; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===44) { + if (_la===49) { { - this.state = 173; + this.state = 183; this.match(esql_parser.NOT); } } - this.state = 176; + this.state = 186; this.match(esql_parser.IN); - this.state = 177; + this.state = 187; this.match(esql_parser.LP); - this.state = 178; + this.state = 188; this.valueExpression(); - this.state = 183; + this.state = 193; this._errHandler.sync(this); _la = this._input.LA(1); - while (_la===34) { + while (_la===39) { { { - this.state = 179; + this.state = 189; this.match(esql_parser.COMMA); - this.state = 180; + this.state = 190; this.valueExpression(); } } - this.state = 185; + this.state = 195; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 186; + this.state = 196; this.match(esql_parser.RP); } break; @@ -802,21 +846,21 @@ export default class esql_parser extends parser_config { localctx = new IsNullContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 188; + this.state = 198; this.valueExpression(); - this.state = 189; + this.state = 199; this.match(esql_parser.IS); - this.state = 191; + this.state = 201; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===44) { + if (_la===49) { { - this.state = 190; + this.state = 200; this.match(esql_parser.NOT); } } - this.state = 193; + this.state = 203; this.match(esql_parser.NULL); } break; @@ -825,17 +869,13 @@ export default class esql_parser extends parser_config { localctx = new MatchExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 195; - if (!(this.isDevVersion())) { - throw this.createFailedPredicateException("this.isDevVersion()"); - } - this.state = 196; + this.state = 205; this.matchBooleanExpression(); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 207; + this.state = 216; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 8, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -845,7 +885,7 @@ export default class esql_parser extends parser_config { } _prevctx = localctx; { - this.state = 205; + this.state = 214; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 7, this._ctx) ) { case 1: @@ -853,13 +893,13 @@ export default class esql_parser extends parser_config { localctx = new LogicalBinaryContext(this, new BooleanExpressionContext(this, _parentctx, _parentState)); (localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 199; + this.state = 208; if (!(this.precpred(this._ctx, 5))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 5)"); } - this.state = 200; + this.state = 209; (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.AND); - this.state = 201; + this.state = 210; (localctx as LogicalBinaryContext)._right = this.booleanExpression(6); } break; @@ -868,20 +908,20 @@ export default class esql_parser extends parser_config { localctx = new LogicalBinaryContext(this, new BooleanExpressionContext(this, _parentctx, _parentState)); (localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 202; + this.state = 211; if (!(this.precpred(this._ctx, 4))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 4)"); } - this.state = 203; + this.state = 212; (localctx as LogicalBinaryContext)._operator = this.match(esql_parser.OR); - this.state = 204; + this.state = 213; (localctx as LogicalBinaryContext)._right = this.booleanExpression(5); } break; } } } - this.state = 209; + this.state = 218; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 8, this._ctx); } @@ -907,48 +947,48 @@ export default class esql_parser extends parser_config { this.enterRule(localctx, 12, esql_parser.RULE_regexBooleanExpression); let _la: number; try { - this.state = 224; + this.state = 233; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 11, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 210; + this.state = 219; this.valueExpression(); - this.state = 212; + this.state = 221; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===44) { + if (_la===49) { { - this.state = 211; + this.state = 220; this.match(esql_parser.NOT); } } - this.state = 214; + this.state = 223; localctx._kind = this.match(esql_parser.LIKE); - this.state = 215; + this.state = 224; localctx._pattern = this.string_(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 217; + this.state = 226; this.valueExpression(); - this.state = 219; + this.state = 228; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===44) { + if (_la===49) { { - this.state = 218; + this.state = 227; this.match(esql_parser.NOT); } } - this.state = 221; + this.state = 230; localctx._kind = this.match(esql_parser.RLIKE); - this.state = 222; + this.state = 231; localctx._pattern = this.string_(); } break; @@ -975,11 +1015,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 226; + this.state = 235; localctx._fieldExp = this.qualifiedName(); - this.state = 227; + this.state = 236; this.match(esql_parser.COLON); - this.state = 228; + this.state = 237; localctx._queryString = this.constant(); } } @@ -1002,14 +1042,14 @@ export default class esql_parser extends parser_config { let localctx: ValueExpressionContext = new ValueExpressionContext(this, this._ctx, this.state); this.enterRule(localctx, 16, esql_parser.RULE_valueExpression); try { - this.state = 235; + this.state = 244; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 12, this._ctx) ) { case 1: localctx = new ValueExpressionDefaultContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 230; + this.state = 239; this.operatorExpression(0); } break; @@ -1017,11 +1057,11 @@ export default class esql_parser extends parser_config { localctx = new ComparisonContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 231; + this.state = 240; (localctx as ComparisonContext)._left = this.operatorExpression(0); - this.state = 232; + this.state = 241; this.comparisonOperator(); - this.state = 233; + this.state = 242; (localctx as ComparisonContext)._right = this.operatorExpression(0); } break; @@ -1061,7 +1101,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 241; + this.state = 250; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 13, this._ctx) ) { case 1: @@ -1070,7 +1110,7 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 238; + this.state = 247; this.primaryExpression(0); } break; @@ -1079,23 +1119,23 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticUnaryContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 239; + this.state = 248; (localctx as ArithmeticUnaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); - if(!(_la===59 || _la===60)) { + if(!(_la===64 || _la===65)) { (localctx as ArithmeticUnaryContext)._operator = this._errHandler.recoverInline(this); } else { this._errHandler.reportMatch(this); this.consume(); } - this.state = 240; + this.state = 249; this.operatorExpression(3); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 251; + this.state = 260; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 15, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -1105,7 +1145,7 @@ export default class esql_parser extends parser_config { } _prevctx = localctx; { - this.state = 249; + this.state = 258; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 14, this._ctx) ) { case 1: @@ -1113,21 +1153,21 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticBinaryContext(this, new OperatorExpressionContext(this, _parentctx, _parentState)); (localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 243; + this.state = 252; if (!(this.precpred(this._ctx, 2))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 2)"); } - this.state = 244; + this.state = 253; (localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); - if(!(((((_la - 61)) & ~0x1F) === 0 && ((1 << (_la - 61)) & 7) !== 0))) { + if(!(((((_la - 66)) & ~0x1F) === 0 && ((1 << (_la - 66)) & 7) !== 0))) { (localctx as ArithmeticBinaryContext)._operator = this._errHandler.recoverInline(this); } else { this._errHandler.reportMatch(this); this.consume(); } - this.state = 245; + this.state = 254; (localctx as ArithmeticBinaryContext)._right = this.operatorExpression(3); } break; @@ -1136,28 +1176,28 @@ export default class esql_parser extends parser_config { localctx = new ArithmeticBinaryContext(this, new OperatorExpressionContext(this, _parentctx, _parentState)); (localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 246; + this.state = 255; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 247; + this.state = 256; (localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); - if(!(_la===59 || _la===60)) { + if(!(_la===64 || _la===65)) { (localctx as ArithmeticBinaryContext)._operator = this._errHandler.recoverInline(this); } else { this._errHandler.reportMatch(this); this.consume(); } - this.state = 248; + this.state = 257; (localctx as ArithmeticBinaryContext)._right = this.operatorExpression(2); } break; } } } - this.state = 253; + this.state = 262; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 15, this._ctx); } @@ -1196,7 +1236,7 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 262; + this.state = 271; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 16, this._ctx) ) { case 1: @@ -1205,7 +1245,7 @@ export default class esql_parser extends parser_config { this._ctx = localctx; _prevctx = localctx; - this.state = 255; + this.state = 264; this.constant(); } break; @@ -1214,7 +1254,7 @@ export default class esql_parser extends parser_config { localctx = new DereferenceContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 256; + this.state = 265; this.qualifiedName(); } break; @@ -1223,7 +1263,7 @@ export default class esql_parser extends parser_config { localctx = new FunctionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 257; + this.state = 266; this.functionExpression(); } break; @@ -1232,17 +1272,17 @@ export default class esql_parser extends parser_config { localctx = new ParenthesizedExpressionContext(this, localctx); this._ctx = localctx; _prevctx = localctx; - this.state = 258; + this.state = 267; this.match(esql_parser.LP); - this.state = 259; + this.state = 268; this.booleanExpression(0); - this.state = 260; + this.state = 269; this.match(esql_parser.RP); } break; } this._ctx.stop = this._input.LT(-1); - this.state = 269; + this.state = 278; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 17, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -1255,18 +1295,18 @@ export default class esql_parser extends parser_config { { localctx = new InlineCastContext(this, new PrimaryExpressionContext(this, _parentctx, _parentState)); this.pushNewRecursionContext(localctx, _startState, esql_parser.RULE_primaryExpression); - this.state = 264; + this.state = 273; if (!(this.precpred(this._ctx, 1))) { throw this.createFailedPredicateException("this.precpred(this._ctx, 1)"); } - this.state = 265; + this.state = 274; this.match(esql_parser.CAST_OP); - this.state = 266; + this.state = 275; this.dataType(); } } } - this.state = 271; + this.state = 280; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 17, this._ctx); } @@ -1294,37 +1334,37 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 272; + this.state = 281; this.functionName(); - this.state = 273; + this.state = 282; this.match(esql_parser.LP); - this.state = 283; + this.state = 292; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 19, this._ctx) ) { case 1: { - this.state = 274; + this.state = 283; this.match(esql_parser.ASTERISK); } break; case 2: { { - this.state = 275; + this.state = 284; this.booleanExpression(0); - this.state = 280; + this.state = 289; this._errHandler.sync(this); _la = this._input.LA(1); - while (_la===34) { + while (_la===39) { { { - this.state = 276; + this.state = 285; this.match(esql_parser.COMMA); - this.state = 277; + this.state = 286; this.booleanExpression(0); } } - this.state = 282; + this.state = 291; this._errHandler.sync(this); _la = this._input.LA(1); } @@ -1332,7 +1372,7 @@ export default class esql_parser extends parser_config { } break; } - this.state = 285; + this.state = 294; this.match(esql_parser.RP); } } @@ -1357,7 +1397,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 287; + this.state = 296; this.identifierOrParameter(); } } @@ -1383,7 +1423,7 @@ export default class esql_parser extends parser_config { localctx = new ToDataTypeContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 289; + this.state = 298; this.identifier(); } } @@ -1408,9 +1448,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 291; + this.state = 300; this.match(esql_parser.ROW); - this.state = 292; + this.state = 301; this.fields(); } } @@ -1436,23 +1476,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 294; + this.state = 303; this.field(); - this.state = 299; + this.state = 308; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 20, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 295; + this.state = 304; this.match(esql_parser.COMMA); - this.state = 296; + this.state = 305; this.field(); } } } - this.state = 301; + this.state = 310; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 20, this._ctx); } @@ -1479,19 +1519,19 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 305; + this.state = 314; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 21, this._ctx) ) { case 1: { - this.state = 302; + this.state = 311; this.qualifiedName(); - this.state = 303; + this.state = 312; this.match(esql_parser.ASSIGN); } break; } - this.state = 307; + this.state = 316; this.booleanExpression(0); } } @@ -1517,34 +1557,34 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 309; + this.state = 318; this.match(esql_parser.FROM); - this.state = 310; + this.state = 319; this.indexPattern(); - this.state = 315; + this.state = 324; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 22, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 311; + this.state = 320; this.match(esql_parser.COMMA); - this.state = 312; + this.state = 321; this.indexPattern(); } } } - this.state = 317; + this.state = 326; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 22, this._ctx); } - this.state = 319; + this.state = 328; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 23, this._ctx) ) { case 1: { - this.state = 318; + this.state = 327; this.metadata(); } break; @@ -1572,19 +1612,19 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 324; + this.state = 333; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 24, this._ctx) ) { case 1: { - this.state = 321; + this.state = 330; this.clusterString(); - this.state = 322; + this.state = 331; this.match(esql_parser.COLON); } break; } - this.state = 326; + this.state = 335; this.indexString(); } } @@ -1609,7 +1649,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 328; + this.state = 337; this.match(esql_parser.UNQUOTED_SOURCE); } } @@ -1635,9 +1675,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 330; + this.state = 339; _la = this._input.LA(1); - if(!(_la===26 || _la===76)) { + if(!(_la===30 || _la===81)) { this._errHandler.recoverInline(this); } else { @@ -1665,20 +1705,20 @@ export default class esql_parser extends parser_config { let localctx: MetadataContext = new MetadataContext(this, this._ctx, this.state); this.enterRule(localctx, 42, esql_parser.RULE_metadata); try { - this.state = 334; + this.state = 343; this._errHandler.sync(this); switch (this._input.LA(1)) { - case 75: + case 80: this.enterOuterAlt(localctx, 1); { - this.state = 332; + this.state = 341; this.metadataOption(); } break; - case 65: + case 70: this.enterOuterAlt(localctx, 2); { - this.state = 333; + this.state = 342; this.deprecated_metadata(); } break; @@ -1708,25 +1748,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 336; + this.state = 345; this.match(esql_parser.METADATA); - this.state = 337; + this.state = 346; this.match(esql_parser.UNQUOTED_SOURCE); - this.state = 342; + this.state = 351; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 26, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 338; + this.state = 347; this.match(esql_parser.COMMA); - this.state = 339; + this.state = 348; this.match(esql_parser.UNQUOTED_SOURCE); } } } - this.state = 344; + this.state = 353; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 26, this._ctx); } @@ -1753,11 +1793,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 345; + this.state = 354; this.match(esql_parser.OPENING_BRACKET); - this.state = 346; + this.state = 355; this.metadataOption(); - this.state = 347; + this.state = 356; this.match(esql_parser.CLOSING_BRACKET); } } @@ -1783,46 +1823,46 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 349; + this.state = 358; this.match(esql_parser.DEV_METRICS); - this.state = 350; + this.state = 359; this.indexPattern(); - this.state = 355; + this.state = 364; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 27, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 351; + this.state = 360; this.match(esql_parser.COMMA); - this.state = 352; + this.state = 361; this.indexPattern(); } } } - this.state = 357; + this.state = 366; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 27, this._ctx); } - this.state = 359; + this.state = 368; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 28, this._ctx) ) { case 1: { - this.state = 358; + this.state = 367; localctx._aggregates = this.aggFields(); } break; } - this.state = 363; + this.state = 372; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 29, this._ctx) ) { case 1: { - this.state = 361; + this.state = 370; this.match(esql_parser.BY); - this.state = 362; + this.state = 371; localctx._grouping = this.fields(); } break; @@ -1850,9 +1890,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 365; + this.state = 374; this.match(esql_parser.EVAL); - this.state = 366; + this.state = 375; this.fields(); } } @@ -1877,26 +1917,26 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 368; + this.state = 377; this.match(esql_parser.STATS); - this.state = 370; + this.state = 379; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 30, this._ctx) ) { case 1: { - this.state = 369; + this.state = 378; localctx._stats = this.aggFields(); } break; } - this.state = 374; + this.state = 383; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 31, this._ctx) ) { case 1: { - this.state = 372; + this.state = 381; this.match(esql_parser.BY); - this.state = 373; + this.state = 382; localctx._grouping = this.fields(); } break; @@ -1925,23 +1965,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 376; + this.state = 385; this.aggField(); - this.state = 381; + this.state = 390; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 377; + this.state = 386; this.match(esql_parser.COMMA); - this.state = 378; + this.state = 387; this.aggField(); } } } - this.state = 383; + this.state = 392; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 32, this._ctx); } @@ -1968,16 +2008,16 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 384; + this.state = 393; this.field(); - this.state = 387; + this.state = 396; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 33, this._ctx) ) { case 1: { - this.state = 385; + this.state = 394; this.match(esql_parser.WHERE); - this.state = 386; + this.state = 395; this.booleanExpression(0); } break; @@ -2006,23 +2046,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 389; + this.state = 398; this.identifierOrParameter(); - this.state = 394; + this.state = 403; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 34, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 390; + this.state = 399; this.match(esql_parser.DOT); - this.state = 391; + this.state = 400; this.identifierOrParameter(); } } } - this.state = 396; + this.state = 405; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 34, this._ctx); } @@ -2050,23 +2090,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 397; + this.state = 406; this.identifierPattern(); - this.state = 402; + this.state = 411; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 35, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 398; + this.state = 407; this.match(esql_parser.DOT); - this.state = 399; + this.state = 408; this.identifierPattern(); } } } - this.state = 404; + this.state = 413; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 35, this._ctx); } @@ -2094,23 +2134,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 405; + this.state = 414; this.qualifiedNamePattern(); - this.state = 410; + this.state = 419; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 36, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 406; + this.state = 415; this.match(esql_parser.COMMA); - this.state = 407; + this.state = 416; this.qualifiedNamePattern(); } } } - this.state = 412; + this.state = 421; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 36, this._ctx); } @@ -2138,9 +2178,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 413; + this.state = 422; _la = this._input.LA(1); - if(!(_la===67 || _la===68)) { + if(!(_la===72 || _la===73)) { this._errHandler.recoverInline(this); } else { @@ -2168,24 +2208,24 @@ export default class esql_parser extends parser_config { let localctx: IdentifierPatternContext = new IdentifierPatternContext(this, this._ctx, this.state); this.enterRule(localctx, 66, esql_parser.RULE_identifierPattern); try { - this.state = 418; + this.state = 427; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 37, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 415; + this.state = 424; this.match(esql_parser.ID_PATTERN); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 416; + this.state = 425; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 417; + this.state = 426; this.parameter(); } break; @@ -2211,14 +2251,14 @@ export default class esql_parser extends parser_config { this.enterRule(localctx, 68, esql_parser.RULE_constant); let _la: number; try { - this.state = 462; + this.state = 471; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 41, this._ctx) ) { case 1: localctx = new NullLiteralContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 420; + this.state = 429; this.match(esql_parser.NULL); } break; @@ -2226,9 +2266,9 @@ export default class esql_parser extends parser_config { localctx = new QualifiedIntegerLiteralContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 421; + this.state = 430; this.integerValue(); - this.state = 422; + this.state = 431; this.match(esql_parser.UNQUOTED_IDENTIFIER); } break; @@ -2236,7 +2276,7 @@ export default class esql_parser extends parser_config { localctx = new DecimalLiteralContext(this, localctx); this.enterOuterAlt(localctx, 3); { - this.state = 424; + this.state = 433; this.decimalValue(); } break; @@ -2244,7 +2284,7 @@ export default class esql_parser extends parser_config { localctx = new IntegerLiteralContext(this, localctx); this.enterOuterAlt(localctx, 4); { - this.state = 425; + this.state = 434; this.integerValue(); } break; @@ -2252,7 +2292,7 @@ export default class esql_parser extends parser_config { localctx = new BooleanLiteralContext(this, localctx); this.enterOuterAlt(localctx, 5); { - this.state = 426; + this.state = 435; this.booleanValue(); } break; @@ -2260,7 +2300,7 @@ export default class esql_parser extends parser_config { localctx = new InputParameterContext(this, localctx); this.enterOuterAlt(localctx, 6); { - this.state = 427; + this.state = 436; this.parameter(); } break; @@ -2268,7 +2308,7 @@ export default class esql_parser extends parser_config { localctx = new StringLiteralContext(this, localctx); this.enterOuterAlt(localctx, 7); { - this.state = 428; + this.state = 437; this.string_(); } break; @@ -2276,27 +2316,27 @@ export default class esql_parser extends parser_config { localctx = new NumericArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 8); { - this.state = 429; + this.state = 438; this.match(esql_parser.OPENING_BRACKET); - this.state = 430; + this.state = 439; this.numericValue(); - this.state = 435; + this.state = 444; this._errHandler.sync(this); _la = this._input.LA(1); - while (_la===34) { + while (_la===39) { { { - this.state = 431; + this.state = 440; this.match(esql_parser.COMMA); - this.state = 432; + this.state = 441; this.numericValue(); } } - this.state = 437; + this.state = 446; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 438; + this.state = 447; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -2304,27 +2344,27 @@ export default class esql_parser extends parser_config { localctx = new BooleanArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 9); { - this.state = 440; + this.state = 449; this.match(esql_parser.OPENING_BRACKET); - this.state = 441; + this.state = 450; this.booleanValue(); - this.state = 446; + this.state = 455; this._errHandler.sync(this); _la = this._input.LA(1); - while (_la===34) { + while (_la===39) { { { - this.state = 442; + this.state = 451; this.match(esql_parser.COMMA); - this.state = 443; + this.state = 452; this.booleanValue(); } } - this.state = 448; + this.state = 457; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 449; + this.state = 458; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -2332,27 +2372,27 @@ export default class esql_parser extends parser_config { localctx = new StringArrayLiteralContext(this, localctx); this.enterOuterAlt(localctx, 10); { - this.state = 451; + this.state = 460; this.match(esql_parser.OPENING_BRACKET); - this.state = 452; + this.state = 461; this.string_(); - this.state = 457; + this.state = 466; this._errHandler.sync(this); _la = this._input.LA(1); - while (_la===34) { + while (_la===39) { { { - this.state = 453; + this.state = 462; this.match(esql_parser.COMMA); - this.state = 454; + this.state = 463; this.string_(); } } - this.state = 459; + this.state = 468; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 460; + this.state = 469; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -2377,22 +2417,22 @@ export default class esql_parser extends parser_config { let localctx: ParameterContext = new ParameterContext(this, this._ctx, this.state); this.enterRule(localctx, 70, esql_parser.RULE_parameter); try { - this.state = 466; + this.state = 475; this._errHandler.sync(this); switch (this._input.LA(1)) { - case 48: + case 53: localctx = new InputParamContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 464; + this.state = 473; this.match(esql_parser.PARAM); } break; - case 64: + case 69: localctx = new InputNamedOrPositionalParamContext(this, localctx); this.enterOuterAlt(localctx, 2); { - this.state = 465; + this.state = 474; this.match(esql_parser.NAMED_OR_POSITIONAL_PARAM); } break; @@ -2419,24 +2459,24 @@ export default class esql_parser extends parser_config { let localctx: IdentifierOrParameterContext = new IdentifierOrParameterContext(this, this._ctx, this.state); this.enterRule(localctx, 72, esql_parser.RULE_identifierOrParameter); try { - this.state = 471; + this.state = 480; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 43, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 468; + this.state = 477; this.identifier(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 469; + this.state = 478; if (!(this.isDevVersion())) { throw this.createFailedPredicateException("this.isDevVersion()"); } - this.state = 470; + this.state = 479; this.parameter(); } break; @@ -2463,9 +2503,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 473; + this.state = 482; this.match(esql_parser.LIMIT); - this.state = 474; + this.state = 483; this.match(esql_parser.INTEGER_LITERAL); } } @@ -2491,25 +2531,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 476; + this.state = 485; this.match(esql_parser.SORT); - this.state = 477; + this.state = 486; this.orderExpression(); - this.state = 482; + this.state = 491; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 44, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 478; + this.state = 487; this.match(esql_parser.COMMA); - this.state = 479; + this.state = 488; this.orderExpression(); } } } - this.state = 484; + this.state = 493; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 44, this._ctx); } @@ -2537,17 +2577,17 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 485; + this.state = 494; this.booleanExpression(0); - this.state = 487; + this.state = 496; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 45, this._ctx) ) { case 1: { - this.state = 486; + this.state = 495; localctx._ordering = this._input.LT(1); _la = this._input.LA(1); - if(!(_la===31 || _la===35)) { + if(!(_la===35 || _la===40)) { localctx._ordering = this._errHandler.recoverInline(this); } else { @@ -2557,17 +2597,17 @@ export default class esql_parser extends parser_config { } break; } - this.state = 491; + this.state = 500; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 46, this._ctx) ) { case 1: { - this.state = 489; + this.state = 498; this.match(esql_parser.NULLS); - this.state = 490; + this.state = 499; localctx._nullOrdering = this._input.LT(1); _la = this._input.LA(1); - if(!(_la===38 || _la===41)) { + if(!(_la===43 || _la===46)) { localctx._nullOrdering = this._errHandler.recoverInline(this); } else { @@ -2600,9 +2640,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 493; + this.state = 502; this.match(esql_parser.KEEP); - this.state = 494; + this.state = 503; this.qualifiedNamePatterns(); } } @@ -2627,9 +2667,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 496; + this.state = 505; this.match(esql_parser.DROP); - this.state = 497; + this.state = 506; this.qualifiedNamePatterns(); } } @@ -2655,25 +2695,25 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 499; + this.state = 508; this.match(esql_parser.RENAME); - this.state = 500; + this.state = 509; this.renameClause(); - this.state = 505; + this.state = 514; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 47, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 501; + this.state = 510; this.match(esql_parser.COMMA); - this.state = 502; + this.state = 511; this.renameClause(); } } } - this.state = 507; + this.state = 516; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 47, this._ctx); } @@ -2700,11 +2740,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 508; + this.state = 517; localctx._oldName = this.qualifiedNamePattern(); - this.state = 509; + this.state = 518; this.match(esql_parser.AS); - this.state = 510; + this.state = 519; localctx._newName = this.qualifiedNamePattern(); } } @@ -2729,18 +2769,18 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 512; + this.state = 521; this.match(esql_parser.DISSECT); - this.state = 513; + this.state = 522; this.primaryExpression(0); - this.state = 514; + this.state = 523; this.string_(); - this.state = 516; + this.state = 525; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 48, this._ctx) ) { case 1: { - this.state = 515; + this.state = 524; this.commandOptions(); } break; @@ -2768,11 +2808,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 518; + this.state = 527; this.match(esql_parser.GROK); - this.state = 519; + this.state = 528; this.primaryExpression(0); - this.state = 520; + this.state = 529; this.string_(); } } @@ -2797,9 +2837,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 522; + this.state = 531; this.match(esql_parser.MV_EXPAND); - this.state = 523; + this.state = 532; this.qualifiedName(); } } @@ -2825,23 +2865,23 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 525; + this.state = 534; this.commandOption(); - this.state = 530; + this.state = 539; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 49, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 526; + this.state = 535; this.match(esql_parser.COMMA); - this.state = 527; + this.state = 536; this.commandOption(); } } } - this.state = 532; + this.state = 541; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 49, this._ctx); } @@ -2868,11 +2908,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 533; + this.state = 542; this.identifier(); - this.state = 534; + this.state = 543; this.match(esql_parser.ASSIGN); - this.state = 535; + this.state = 544; this.constant(); } } @@ -2898,9 +2938,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 537; + this.state = 546; _la = this._input.LA(1); - if(!(_la===37 || _la===51)) { + if(!(_la===42 || _la===56)) { this._errHandler.recoverInline(this); } else { @@ -2928,20 +2968,20 @@ export default class esql_parser extends parser_config { let localctx: NumericValueContext = new NumericValueContext(this, this._ctx, this.state); this.enterRule(localctx, 100, esql_parser.RULE_numericValue); try { - this.state = 541; + this.state = 550; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 50, this._ctx) ) { case 1: this.enterOuterAlt(localctx, 1); { - this.state = 539; + this.state = 548; this.decimalValue(); } break; case 2: this.enterOuterAlt(localctx, 2); { - this.state = 540; + this.state = 549; this.integerValue(); } break; @@ -2969,14 +3009,14 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 544; + this.state = 553; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===59 || _la===60) { + if (_la===64 || _la===65) { { - this.state = 543; + this.state = 552; _la = this._input.LA(1); - if(!(_la===59 || _la===60)) { + if(!(_la===64 || _la===65)) { this._errHandler.recoverInline(this); } else { @@ -2986,7 +3026,7 @@ export default class esql_parser extends parser_config { } } - this.state = 546; + this.state = 555; this.match(esql_parser.DECIMAL_LITERAL); } } @@ -3012,14 +3052,14 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 549; + this.state = 558; this._errHandler.sync(this); _la = this._input.LA(1); - if (_la===59 || _la===60) { + if (_la===64 || _la===65) { { - this.state = 548; + this.state = 557; _la = this._input.LA(1); - if(!(_la===59 || _la===60)) { + if(!(_la===64 || _la===65)) { this._errHandler.recoverInline(this); } else { @@ -3029,7 +3069,7 @@ export default class esql_parser extends parser_config { } } - this.state = 551; + this.state = 560; this.match(esql_parser.INTEGER_LITERAL); } } @@ -3054,7 +3094,7 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 553; + this.state = 562; this.match(esql_parser.QUOTED_STRING); } } @@ -3080,9 +3120,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 555; + this.state = 564; _la = this._input.LA(1); - if(!(((((_la - 52)) & ~0x1F) === 0 && ((1 << (_la - 52)) & 125) !== 0))) { + if(!(((((_la - 57)) & ~0x1F) === 0 && ((1 << (_la - 57)) & 125) !== 0))) { this._errHandler.recoverInline(this); } else { @@ -3112,9 +3152,9 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 557; + this.state = 566; this.match(esql_parser.EXPLAIN); - this.state = 558; + this.state = 567; this.subqueryExpression(); } } @@ -3139,11 +3179,11 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 560; + this.state = 569; this.match(esql_parser.OPENING_BRACKET); - this.state = 561; + this.state = 570; this.query(0); - this.state = 562; + this.state = 571; this.match(esql_parser.CLOSING_BRACKET); } } @@ -3169,9 +3209,9 @@ export default class esql_parser extends parser_config { localctx = new ShowInfoContext(this, localctx); this.enterOuterAlt(localctx, 1); { - this.state = 564; + this.state = 573; this.match(esql_parser.SHOW); - this.state = 565; + this.state = 574; this.match(esql_parser.INFO); } } @@ -3197,46 +3237,46 @@ export default class esql_parser extends parser_config { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 567; + this.state = 576; this.match(esql_parser.ENRICH); - this.state = 568; + this.state = 577; localctx._policyName = this.match(esql_parser.ENRICH_POLICY_NAME); - this.state = 571; + this.state = 580; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 53, this._ctx) ) { case 1: { - this.state = 569; + this.state = 578; this.match(esql_parser.ON); - this.state = 570; + this.state = 579; localctx._matchField = this.qualifiedNamePattern(); } break; } - this.state = 582; + this.state = 591; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 55, this._ctx) ) { case 1: { - this.state = 573; + this.state = 582; this.match(esql_parser.WITH); - this.state = 574; + this.state = 583; this.enrichWithClause(); - this.state = 579; + this.state = 588; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 54, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 575; + this.state = 584; this.match(esql_parser.COMMA); - this.state = 576; + this.state = 585; this.enrichWithClause(); } } } - this.state = 581; + this.state = 590; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 54, this._ctx); } @@ -3266,19 +3306,19 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 587; + this.state = 596; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 56, this._ctx) ) { case 1: { - this.state = 584; + this.state = 593; localctx._newName = this.qualifiedNamePattern(); - this.state = 585; + this.state = 594; this.match(esql_parser.ASSIGN); } break; } - this.state = 589; + this.state = 598; localctx._enrichField = this.qualifiedNamePattern(); } } @@ -3303,13 +3343,13 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 591; + this.state = 600; this.match(esql_parser.DEV_LOOKUP); - this.state = 592; + this.state = 601; localctx._tableName = this.indexPattern(); - this.state = 593; + this.state = 602; this.match(esql_parser.ON); - this.state = 594; + this.state = 603; localctx._matchFields = this.qualifiedNamePatterns(); } } @@ -3334,18 +3374,18 @@ export default class esql_parser extends parser_config { try { this.enterOuterAlt(localctx, 1); { - this.state = 596; + this.state = 605; this.match(esql_parser.DEV_INLINESTATS); - this.state = 597; + this.state = 606; localctx._stats = this.aggFields(); - this.state = 600; + this.state = 609; this._errHandler.sync(this); switch ( this._interp.adaptivePredict(this._input, 57, this._ctx) ) { case 1: { - this.state = 598; + this.state = 607; this.match(esql_parser.BY); - this.state = 599; + this.state = 608; localctx._grouping = this.fields(); } break; @@ -3366,6 +3406,163 @@ export default class esql_parser extends parser_config { } return localctx; } + // @RuleVersion(0) + public joinCommand(): JoinCommandContext { + let localctx: JoinCommandContext = new JoinCommandContext(this, this._ctx, this.state); + this.enterRule(localctx, 124, esql_parser.RULE_joinCommand); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 612; + this._errHandler.sync(this); + _la = this._input.LA(1); + if ((((_la) & ~0x1F) === 0 && ((1 << _la) & 29360128) !== 0)) { + { + this.state = 611; + localctx._type_ = this._input.LT(1); + _la = this._input.LA(1); + if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 29360128) !== 0))) { + localctx._type_ = this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + + this.state = 614; + this.match(esql_parser.DEV_JOIN); + this.state = 615; + this.joinTarget(); + this.state = 616; + this.joinCondition(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public joinTarget(): JoinTargetContext { + let localctx: JoinTargetContext = new JoinTargetContext(this, this._ctx, this.state); + this.enterRule(localctx, 126, esql_parser.RULE_joinTarget); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 618; + localctx._index = this.identifier(); + this.state = 621; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la===89) { + { + this.state = 619; + this.match(esql_parser.AS); + this.state = 620; + localctx._alias = this.identifier(); + } + } + + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public joinCondition(): JoinConditionContext { + let localctx: JoinConditionContext = new JoinConditionContext(this, this._ctx, this.state); + this.enterRule(localctx, 128, esql_parser.RULE_joinCondition); + try { + let _alt: number; + this.enterOuterAlt(localctx, 1); + { + this.state = 623; + this.match(esql_parser.ON); + this.state = 624; + this.joinPredicate(); + this.state = 629; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 60, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 625; + this.match(esql_parser.COMMA); + this.state = 626; + this.joinPredicate(); + } + } + } + this.state = 631; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 60, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public joinPredicate(): JoinPredicateContext { + let localctx: JoinPredicateContext = new JoinPredicateContext(this, this._ctx, this.state); + this.enterRule(localctx, 130, esql_parser.RULE_joinPredicate); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 632; + this.valueExpression(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } public sempred(localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { switch (ruleIndex) { @@ -3408,13 +3605,13 @@ export default class esql_parser extends parser_config { return this.isDevVersion(); case 3: return this.isDevVersion(); + case 4: + return this.isDevVersion(); } return true; } private booleanExpression_sempred(localctx: BooleanExpressionContext, predIndex: number): boolean { switch (predIndex) { - case 4: - return this.isDevVersion(); case 5: return this.precpred(this._ctx, 5); case 6: @@ -3453,7 +3650,7 @@ export default class esql_parser extends parser_config { return true; } - public static readonly _serializedATN: number[] = [4,1,119,603,2,0,7,0, + public static readonly _serializedATN: number[] = [4,1,128,635,2,0,7,0, 2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9, 2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2, 17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24, @@ -3462,194 +3659,204 @@ export default class esql_parser extends parser_config { 2,39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7,45,2, 46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7,52,2,53, 7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2,59,7,59,2,60,7, - 60,2,61,7,61,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,5,1,134,8,1,10,1,12,1, - 137,9,1,1,2,1,2,1,2,1,2,1,2,1,2,3,2,145,8,2,1,3,1,3,1,3,1,3,1,3,1,3,1,3, - 1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,3,3,163,8,3,1,4,1,4,1,4,1,5,1,5,1,5, - 1,5,1,5,1,5,1,5,3,5,175,8,5,1,5,1,5,1,5,1,5,1,5,5,5,182,8,5,10,5,12,5,185, - 9,5,1,5,1,5,1,5,1,5,1,5,3,5,192,8,5,1,5,1,5,1,5,1,5,3,5,198,8,5,1,5,1,5, - 1,5,1,5,1,5,1,5,5,5,206,8,5,10,5,12,5,209,9,5,1,6,1,6,3,6,213,8,6,1,6,1, - 6,1,6,1,6,1,6,3,6,220,8,6,1,6,1,6,1,6,3,6,225,8,6,1,7,1,7,1,7,1,7,1,8,1, - 8,1,8,1,8,1,8,3,8,236,8,8,1,9,1,9,1,9,1,9,3,9,242,8,9,1,9,1,9,1,9,1,9,1, - 9,1,9,5,9,250,8,9,10,9,12,9,253,9,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10, - 1,10,3,10,263,8,10,1,10,1,10,1,10,5,10,268,8,10,10,10,12,10,271,9,10,1, - 11,1,11,1,11,1,11,1,11,1,11,5,11,279,8,11,10,11,12,11,282,9,11,3,11,284, - 8,11,1,11,1,11,1,12,1,12,1,13,1,13,1,14,1,14,1,14,1,15,1,15,1,15,5,15,298, - 8,15,10,15,12,15,301,9,15,1,16,1,16,1,16,3,16,306,8,16,1,16,1,16,1,17,1, - 17,1,17,1,17,5,17,314,8,17,10,17,12,17,317,9,17,1,17,3,17,320,8,17,1,18, - 1,18,1,18,3,18,325,8,18,1,18,1,18,1,19,1,19,1,20,1,20,1,21,1,21,3,21,335, - 8,21,1,22,1,22,1,22,1,22,5,22,341,8,22,10,22,12,22,344,9,22,1,23,1,23,1, - 23,1,23,1,24,1,24,1,24,1,24,5,24,354,8,24,10,24,12,24,357,9,24,1,24,3,24, - 360,8,24,1,24,1,24,3,24,364,8,24,1,25,1,25,1,25,1,26,1,26,3,26,371,8,26, - 1,26,1,26,3,26,375,8,26,1,27,1,27,1,27,5,27,380,8,27,10,27,12,27,383,9, - 27,1,28,1,28,1,28,3,28,388,8,28,1,29,1,29,1,29,5,29,393,8,29,10,29,12,29, - 396,9,29,1,30,1,30,1,30,5,30,401,8,30,10,30,12,30,404,9,30,1,31,1,31,1, - 31,5,31,409,8,31,10,31,12,31,412,9,31,1,32,1,32,1,33,1,33,1,33,3,33,419, - 8,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,5, - 34,434,8,34,10,34,12,34,437,9,34,1,34,1,34,1,34,1,34,1,34,1,34,5,34,445, - 8,34,10,34,12,34,448,9,34,1,34,1,34,1,34,1,34,1,34,1,34,5,34,456,8,34,10, - 34,12,34,459,9,34,1,34,1,34,3,34,463,8,34,1,35,1,35,3,35,467,8,35,1,36, - 1,36,1,36,3,36,472,8,36,1,37,1,37,1,37,1,38,1,38,1,38,1,38,5,38,481,8,38, - 10,38,12,38,484,9,38,1,39,1,39,3,39,488,8,39,1,39,1,39,3,39,492,8,39,1, - 40,1,40,1,40,1,41,1,41,1,41,1,42,1,42,1,42,1,42,5,42,504,8,42,10,42,12, - 42,507,9,42,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,44,3,44,517,8,44,1,45, - 1,45,1,45,1,45,1,46,1,46,1,46,1,47,1,47,1,47,5,47,529,8,47,10,47,12,47, - 532,9,47,1,48,1,48,1,48,1,48,1,49,1,49,1,50,1,50,3,50,542,8,50,1,51,3,51, - 545,8,51,1,51,1,51,1,52,3,52,550,8,52,1,52,1,52,1,53,1,53,1,54,1,54,1,55, - 1,55,1,55,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1,58,1,58,1,58,1,58,3,58,572, - 8,58,1,58,1,58,1,58,1,58,5,58,578,8,58,10,58,12,58,581,9,58,3,58,583,8, - 58,1,59,1,59,1,59,3,59,588,8,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,61, - 1,61,1,61,1,61,3,61,601,8,61,1,61,0,4,2,10,18,20,62,0,2,4,6,8,10,12,14, - 16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62, - 64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108, - 110,112,114,116,118,120,122,0,8,1,0,59,60,1,0,61,63,2,0,26,26,76,76,1,0, - 67,68,2,0,31,31,35,35,2,0,38,38,41,41,2,0,37,37,51,51,2,0,52,52,54,58,628, - 0,124,1,0,0,0,2,127,1,0,0,0,4,144,1,0,0,0,6,162,1,0,0,0,8,164,1,0,0,0,10, - 197,1,0,0,0,12,224,1,0,0,0,14,226,1,0,0,0,16,235,1,0,0,0,18,241,1,0,0,0, - 20,262,1,0,0,0,22,272,1,0,0,0,24,287,1,0,0,0,26,289,1,0,0,0,28,291,1,0, - 0,0,30,294,1,0,0,0,32,305,1,0,0,0,34,309,1,0,0,0,36,324,1,0,0,0,38,328, - 1,0,0,0,40,330,1,0,0,0,42,334,1,0,0,0,44,336,1,0,0,0,46,345,1,0,0,0,48, - 349,1,0,0,0,50,365,1,0,0,0,52,368,1,0,0,0,54,376,1,0,0,0,56,384,1,0,0,0, - 58,389,1,0,0,0,60,397,1,0,0,0,62,405,1,0,0,0,64,413,1,0,0,0,66,418,1,0, - 0,0,68,462,1,0,0,0,70,466,1,0,0,0,72,471,1,0,0,0,74,473,1,0,0,0,76,476, - 1,0,0,0,78,485,1,0,0,0,80,493,1,0,0,0,82,496,1,0,0,0,84,499,1,0,0,0,86, - 508,1,0,0,0,88,512,1,0,0,0,90,518,1,0,0,0,92,522,1,0,0,0,94,525,1,0,0,0, - 96,533,1,0,0,0,98,537,1,0,0,0,100,541,1,0,0,0,102,544,1,0,0,0,104,549,1, - 0,0,0,106,553,1,0,0,0,108,555,1,0,0,0,110,557,1,0,0,0,112,560,1,0,0,0,114, - 564,1,0,0,0,116,567,1,0,0,0,118,587,1,0,0,0,120,591,1,0,0,0,122,596,1,0, - 0,0,124,125,3,2,1,0,125,126,5,0,0,1,126,1,1,0,0,0,127,128,6,1,-1,0,128, - 129,3,4,2,0,129,135,1,0,0,0,130,131,10,1,0,0,131,132,5,25,0,0,132,134,3, - 6,3,0,133,130,1,0,0,0,134,137,1,0,0,0,135,133,1,0,0,0,135,136,1,0,0,0,136, - 3,1,0,0,0,137,135,1,0,0,0,138,145,3,110,55,0,139,145,3,34,17,0,140,145, - 3,28,14,0,141,145,3,114,57,0,142,143,4,2,1,0,143,145,3,48,24,0,144,138, - 1,0,0,0,144,139,1,0,0,0,144,140,1,0,0,0,144,141,1,0,0,0,144,142,1,0,0,0, - 145,5,1,0,0,0,146,163,3,50,25,0,147,163,3,8,4,0,148,163,3,80,40,0,149,163, - 3,74,37,0,150,163,3,52,26,0,151,163,3,76,38,0,152,163,3,82,41,0,153,163, - 3,84,42,0,154,163,3,88,44,0,155,163,3,90,45,0,156,163,3,116,58,0,157,163, - 3,92,46,0,158,159,4,3,2,0,159,163,3,122,61,0,160,161,4,3,3,0,161,163,3, - 120,60,0,162,146,1,0,0,0,162,147,1,0,0,0,162,148,1,0,0,0,162,149,1,0,0, - 0,162,150,1,0,0,0,162,151,1,0,0,0,162,152,1,0,0,0,162,153,1,0,0,0,162,154, - 1,0,0,0,162,155,1,0,0,0,162,156,1,0,0,0,162,157,1,0,0,0,162,158,1,0,0,0, - 162,160,1,0,0,0,163,7,1,0,0,0,164,165,5,16,0,0,165,166,3,10,5,0,166,9,1, - 0,0,0,167,168,6,5,-1,0,168,169,5,44,0,0,169,198,3,10,5,8,170,198,3,16,8, - 0,171,198,3,12,6,0,172,174,3,16,8,0,173,175,5,44,0,0,174,173,1,0,0,0,174, - 175,1,0,0,0,175,176,1,0,0,0,176,177,5,39,0,0,177,178,5,43,0,0,178,183,3, - 16,8,0,179,180,5,34,0,0,180,182,3,16,8,0,181,179,1,0,0,0,182,185,1,0,0, - 0,183,181,1,0,0,0,183,184,1,0,0,0,184,186,1,0,0,0,185,183,1,0,0,0,186,187, - 5,50,0,0,187,198,1,0,0,0,188,189,3,16,8,0,189,191,5,40,0,0,190,192,5,44, - 0,0,191,190,1,0,0,0,191,192,1,0,0,0,192,193,1,0,0,0,193,194,5,45,0,0,194, - 198,1,0,0,0,195,196,4,5,4,0,196,198,3,14,7,0,197,167,1,0,0,0,197,170,1, - 0,0,0,197,171,1,0,0,0,197,172,1,0,0,0,197,188,1,0,0,0,197,195,1,0,0,0,198, - 207,1,0,0,0,199,200,10,5,0,0,200,201,5,30,0,0,201,206,3,10,5,6,202,203, - 10,4,0,0,203,204,5,47,0,0,204,206,3,10,5,5,205,199,1,0,0,0,205,202,1,0, - 0,0,206,209,1,0,0,0,207,205,1,0,0,0,207,208,1,0,0,0,208,11,1,0,0,0,209, - 207,1,0,0,0,210,212,3,16,8,0,211,213,5,44,0,0,212,211,1,0,0,0,212,213,1, - 0,0,0,213,214,1,0,0,0,214,215,5,42,0,0,215,216,3,106,53,0,216,225,1,0,0, - 0,217,219,3,16,8,0,218,220,5,44,0,0,219,218,1,0,0,0,219,220,1,0,0,0,220, - 221,1,0,0,0,221,222,5,49,0,0,222,223,3,106,53,0,223,225,1,0,0,0,224,210, - 1,0,0,0,224,217,1,0,0,0,225,13,1,0,0,0,226,227,3,58,29,0,227,228,5,24,0, - 0,228,229,3,68,34,0,229,15,1,0,0,0,230,236,3,18,9,0,231,232,3,18,9,0,232, - 233,3,108,54,0,233,234,3,18,9,0,234,236,1,0,0,0,235,230,1,0,0,0,235,231, - 1,0,0,0,236,17,1,0,0,0,237,238,6,9,-1,0,238,242,3,20,10,0,239,240,7,0,0, - 0,240,242,3,18,9,3,241,237,1,0,0,0,241,239,1,0,0,0,242,251,1,0,0,0,243, - 244,10,2,0,0,244,245,7,1,0,0,245,250,3,18,9,3,246,247,10,1,0,0,247,248, - 7,0,0,0,248,250,3,18,9,2,249,243,1,0,0,0,249,246,1,0,0,0,250,253,1,0,0, - 0,251,249,1,0,0,0,251,252,1,0,0,0,252,19,1,0,0,0,253,251,1,0,0,0,254,255, - 6,10,-1,0,255,263,3,68,34,0,256,263,3,58,29,0,257,263,3,22,11,0,258,259, - 5,43,0,0,259,260,3,10,5,0,260,261,5,50,0,0,261,263,1,0,0,0,262,254,1,0, - 0,0,262,256,1,0,0,0,262,257,1,0,0,0,262,258,1,0,0,0,263,269,1,0,0,0,264, - 265,10,1,0,0,265,266,5,33,0,0,266,268,3,26,13,0,267,264,1,0,0,0,268,271, - 1,0,0,0,269,267,1,0,0,0,269,270,1,0,0,0,270,21,1,0,0,0,271,269,1,0,0,0, - 272,273,3,24,12,0,273,283,5,43,0,0,274,284,5,61,0,0,275,280,3,10,5,0,276, - 277,5,34,0,0,277,279,3,10,5,0,278,276,1,0,0,0,279,282,1,0,0,0,280,278,1, - 0,0,0,280,281,1,0,0,0,281,284,1,0,0,0,282,280,1,0,0,0,283,274,1,0,0,0,283, - 275,1,0,0,0,283,284,1,0,0,0,284,285,1,0,0,0,285,286,5,50,0,0,286,23,1,0, - 0,0,287,288,3,72,36,0,288,25,1,0,0,0,289,290,3,64,32,0,290,27,1,0,0,0,291, - 292,5,12,0,0,292,293,3,30,15,0,293,29,1,0,0,0,294,299,3,32,16,0,295,296, - 5,34,0,0,296,298,3,32,16,0,297,295,1,0,0,0,298,301,1,0,0,0,299,297,1,0, - 0,0,299,300,1,0,0,0,300,31,1,0,0,0,301,299,1,0,0,0,302,303,3,58,29,0,303, - 304,5,32,0,0,304,306,1,0,0,0,305,302,1,0,0,0,305,306,1,0,0,0,306,307,1, - 0,0,0,307,308,3,10,5,0,308,33,1,0,0,0,309,310,5,6,0,0,310,315,3,36,18,0, - 311,312,5,34,0,0,312,314,3,36,18,0,313,311,1,0,0,0,314,317,1,0,0,0,315, - 313,1,0,0,0,315,316,1,0,0,0,316,319,1,0,0,0,317,315,1,0,0,0,318,320,3,42, - 21,0,319,318,1,0,0,0,319,320,1,0,0,0,320,35,1,0,0,0,321,322,3,38,19,0,322, - 323,5,24,0,0,323,325,1,0,0,0,324,321,1,0,0,0,324,325,1,0,0,0,325,326,1, - 0,0,0,326,327,3,40,20,0,327,37,1,0,0,0,328,329,5,76,0,0,329,39,1,0,0,0, - 330,331,7,2,0,0,331,41,1,0,0,0,332,335,3,44,22,0,333,335,3,46,23,0,334, - 332,1,0,0,0,334,333,1,0,0,0,335,43,1,0,0,0,336,337,5,75,0,0,337,342,5,76, - 0,0,338,339,5,34,0,0,339,341,5,76,0,0,340,338,1,0,0,0,341,344,1,0,0,0,342, - 340,1,0,0,0,342,343,1,0,0,0,343,45,1,0,0,0,344,342,1,0,0,0,345,346,5,65, - 0,0,346,347,3,44,22,0,347,348,5,66,0,0,348,47,1,0,0,0,349,350,5,19,0,0, - 350,355,3,36,18,0,351,352,5,34,0,0,352,354,3,36,18,0,353,351,1,0,0,0,354, - 357,1,0,0,0,355,353,1,0,0,0,355,356,1,0,0,0,356,359,1,0,0,0,357,355,1,0, - 0,0,358,360,3,54,27,0,359,358,1,0,0,0,359,360,1,0,0,0,360,363,1,0,0,0,361, - 362,5,29,0,0,362,364,3,30,15,0,363,361,1,0,0,0,363,364,1,0,0,0,364,49,1, - 0,0,0,365,366,5,4,0,0,366,367,3,30,15,0,367,51,1,0,0,0,368,370,5,15,0,0, - 369,371,3,54,27,0,370,369,1,0,0,0,370,371,1,0,0,0,371,374,1,0,0,0,372,373, - 5,29,0,0,373,375,3,30,15,0,374,372,1,0,0,0,374,375,1,0,0,0,375,53,1,0,0, - 0,376,381,3,56,28,0,377,378,5,34,0,0,378,380,3,56,28,0,379,377,1,0,0,0, - 380,383,1,0,0,0,381,379,1,0,0,0,381,382,1,0,0,0,382,55,1,0,0,0,383,381, - 1,0,0,0,384,387,3,32,16,0,385,386,5,16,0,0,386,388,3,10,5,0,387,385,1,0, - 0,0,387,388,1,0,0,0,388,57,1,0,0,0,389,394,3,72,36,0,390,391,5,36,0,0,391, - 393,3,72,36,0,392,390,1,0,0,0,393,396,1,0,0,0,394,392,1,0,0,0,394,395,1, - 0,0,0,395,59,1,0,0,0,396,394,1,0,0,0,397,402,3,66,33,0,398,399,5,36,0,0, - 399,401,3,66,33,0,400,398,1,0,0,0,401,404,1,0,0,0,402,400,1,0,0,0,402,403, - 1,0,0,0,403,61,1,0,0,0,404,402,1,0,0,0,405,410,3,60,30,0,406,407,5,34,0, - 0,407,409,3,60,30,0,408,406,1,0,0,0,409,412,1,0,0,0,410,408,1,0,0,0,410, - 411,1,0,0,0,411,63,1,0,0,0,412,410,1,0,0,0,413,414,7,3,0,0,414,65,1,0,0, - 0,415,419,5,80,0,0,416,417,4,33,10,0,417,419,3,70,35,0,418,415,1,0,0,0, - 418,416,1,0,0,0,419,67,1,0,0,0,420,463,5,45,0,0,421,422,3,104,52,0,422, - 423,5,67,0,0,423,463,1,0,0,0,424,463,3,102,51,0,425,463,3,104,52,0,426, - 463,3,98,49,0,427,463,3,70,35,0,428,463,3,106,53,0,429,430,5,65,0,0,430, - 435,3,100,50,0,431,432,5,34,0,0,432,434,3,100,50,0,433,431,1,0,0,0,434, - 437,1,0,0,0,435,433,1,0,0,0,435,436,1,0,0,0,436,438,1,0,0,0,437,435,1,0, - 0,0,438,439,5,66,0,0,439,463,1,0,0,0,440,441,5,65,0,0,441,446,3,98,49,0, - 442,443,5,34,0,0,443,445,3,98,49,0,444,442,1,0,0,0,445,448,1,0,0,0,446, - 444,1,0,0,0,446,447,1,0,0,0,447,449,1,0,0,0,448,446,1,0,0,0,449,450,5,66, - 0,0,450,463,1,0,0,0,451,452,5,65,0,0,452,457,3,106,53,0,453,454,5,34,0, - 0,454,456,3,106,53,0,455,453,1,0,0,0,456,459,1,0,0,0,457,455,1,0,0,0,457, - 458,1,0,0,0,458,460,1,0,0,0,459,457,1,0,0,0,460,461,5,66,0,0,461,463,1, - 0,0,0,462,420,1,0,0,0,462,421,1,0,0,0,462,424,1,0,0,0,462,425,1,0,0,0,462, - 426,1,0,0,0,462,427,1,0,0,0,462,428,1,0,0,0,462,429,1,0,0,0,462,440,1,0, - 0,0,462,451,1,0,0,0,463,69,1,0,0,0,464,467,5,48,0,0,465,467,5,64,0,0,466, - 464,1,0,0,0,466,465,1,0,0,0,467,71,1,0,0,0,468,472,3,64,32,0,469,470,4, - 36,11,0,470,472,3,70,35,0,471,468,1,0,0,0,471,469,1,0,0,0,472,73,1,0,0, - 0,473,474,5,9,0,0,474,475,5,27,0,0,475,75,1,0,0,0,476,477,5,14,0,0,477, - 482,3,78,39,0,478,479,5,34,0,0,479,481,3,78,39,0,480,478,1,0,0,0,481,484, - 1,0,0,0,482,480,1,0,0,0,482,483,1,0,0,0,483,77,1,0,0,0,484,482,1,0,0,0, - 485,487,3,10,5,0,486,488,7,4,0,0,487,486,1,0,0,0,487,488,1,0,0,0,488,491, - 1,0,0,0,489,490,5,46,0,0,490,492,7,5,0,0,491,489,1,0,0,0,491,492,1,0,0, - 0,492,79,1,0,0,0,493,494,5,8,0,0,494,495,3,62,31,0,495,81,1,0,0,0,496,497, - 5,2,0,0,497,498,3,62,31,0,498,83,1,0,0,0,499,500,5,11,0,0,500,505,3,86, - 43,0,501,502,5,34,0,0,502,504,3,86,43,0,503,501,1,0,0,0,504,507,1,0,0,0, - 505,503,1,0,0,0,505,506,1,0,0,0,506,85,1,0,0,0,507,505,1,0,0,0,508,509, - 3,60,30,0,509,510,5,84,0,0,510,511,3,60,30,0,511,87,1,0,0,0,512,513,5,1, - 0,0,513,514,3,20,10,0,514,516,3,106,53,0,515,517,3,94,47,0,516,515,1,0, - 0,0,516,517,1,0,0,0,517,89,1,0,0,0,518,519,5,7,0,0,519,520,3,20,10,0,520, - 521,3,106,53,0,521,91,1,0,0,0,522,523,5,10,0,0,523,524,3,58,29,0,524,93, - 1,0,0,0,525,530,3,96,48,0,526,527,5,34,0,0,527,529,3,96,48,0,528,526,1, - 0,0,0,529,532,1,0,0,0,530,528,1,0,0,0,530,531,1,0,0,0,531,95,1,0,0,0,532, - 530,1,0,0,0,533,534,3,64,32,0,534,535,5,32,0,0,535,536,3,68,34,0,536,97, - 1,0,0,0,537,538,7,6,0,0,538,99,1,0,0,0,539,542,3,102,51,0,540,542,3,104, - 52,0,541,539,1,0,0,0,541,540,1,0,0,0,542,101,1,0,0,0,543,545,7,0,0,0,544, - 543,1,0,0,0,544,545,1,0,0,0,545,546,1,0,0,0,546,547,5,28,0,0,547,103,1, - 0,0,0,548,550,7,0,0,0,549,548,1,0,0,0,549,550,1,0,0,0,550,551,1,0,0,0,551, - 552,5,27,0,0,552,105,1,0,0,0,553,554,5,26,0,0,554,107,1,0,0,0,555,556,7, - 7,0,0,556,109,1,0,0,0,557,558,5,5,0,0,558,559,3,112,56,0,559,111,1,0,0, - 0,560,561,5,65,0,0,561,562,3,2,1,0,562,563,5,66,0,0,563,113,1,0,0,0,564, - 565,5,13,0,0,565,566,5,100,0,0,566,115,1,0,0,0,567,568,5,3,0,0,568,571, - 5,90,0,0,569,570,5,88,0,0,570,572,3,60,30,0,571,569,1,0,0,0,571,572,1,0, - 0,0,572,582,1,0,0,0,573,574,5,89,0,0,574,579,3,118,59,0,575,576,5,34,0, - 0,576,578,3,118,59,0,577,575,1,0,0,0,578,581,1,0,0,0,579,577,1,0,0,0,579, - 580,1,0,0,0,580,583,1,0,0,0,581,579,1,0,0,0,582,573,1,0,0,0,582,583,1,0, - 0,0,583,117,1,0,0,0,584,585,3,60,30,0,585,586,5,32,0,0,586,588,1,0,0,0, - 587,584,1,0,0,0,587,588,1,0,0,0,588,589,1,0,0,0,589,590,3,60,30,0,590,119, - 1,0,0,0,591,592,5,18,0,0,592,593,3,36,18,0,593,594,5,88,0,0,594,595,3,62, - 31,0,595,121,1,0,0,0,596,597,5,17,0,0,597,600,3,54,27,0,598,599,5,29,0, - 0,599,601,3,30,15,0,600,598,1,0,0,0,600,601,1,0,0,0,601,123,1,0,0,0,58, - 135,144,162,174,183,191,197,205,207,212,219,224,235,241,249,251,262,269, - 280,283,299,305,315,319,324,334,342,355,359,363,370,374,381,387,394,402, - 410,418,435,446,457,462,466,471,482,487,491,505,516,530,541,544,549,571, - 579,582,587,600]; + 60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2,65,7,65,1,0,1,0,1,0,1,1,1, + 1,1,1,1,1,1,1,1,1,5,1,142,8,1,10,1,12,1,145,9,1,1,2,1,2,1,2,1,2,1,2,1,2, + 3,2,153,8,2,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3, + 1,3,1,3,1,3,3,3,173,8,3,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,185, + 8,5,1,5,1,5,1,5,1,5,1,5,5,5,192,8,5,10,5,12,5,195,9,5,1,5,1,5,1,5,1,5,1, + 5,3,5,202,8,5,1,5,1,5,1,5,3,5,207,8,5,1,5,1,5,1,5,1,5,1,5,1,5,5,5,215,8, + 5,10,5,12,5,218,9,5,1,6,1,6,3,6,222,8,6,1,6,1,6,1,6,1,6,1,6,3,6,229,8,6, + 1,6,1,6,1,6,3,6,234,8,6,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,3,8,245,8,8, + 1,9,1,9,1,9,1,9,3,9,251,8,9,1,9,1,9,1,9,1,9,1,9,1,9,5,9,259,8,9,10,9,12, + 9,262,9,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,3,10,272,8,10,1,10,1, + 10,1,10,5,10,277,8,10,10,10,12,10,280,9,10,1,11,1,11,1,11,1,11,1,11,1,11, + 5,11,288,8,11,10,11,12,11,291,9,11,3,11,293,8,11,1,11,1,11,1,12,1,12,1, + 13,1,13,1,14,1,14,1,14,1,15,1,15,1,15,5,15,307,8,15,10,15,12,15,310,9,15, + 1,16,1,16,1,16,3,16,315,8,16,1,16,1,16,1,17,1,17,1,17,1,17,5,17,323,8,17, + 10,17,12,17,326,9,17,1,17,3,17,329,8,17,1,18,1,18,1,18,3,18,334,8,18,1, + 18,1,18,1,19,1,19,1,20,1,20,1,21,1,21,3,21,344,8,21,1,22,1,22,1,22,1,22, + 5,22,350,8,22,10,22,12,22,353,9,22,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1, + 24,5,24,363,8,24,10,24,12,24,366,9,24,1,24,3,24,369,8,24,1,24,1,24,3,24, + 373,8,24,1,25,1,25,1,25,1,26,1,26,3,26,380,8,26,1,26,1,26,3,26,384,8,26, + 1,27,1,27,1,27,5,27,389,8,27,10,27,12,27,392,9,27,1,28,1,28,1,28,3,28,397, + 8,28,1,29,1,29,1,29,5,29,402,8,29,10,29,12,29,405,9,29,1,30,1,30,1,30,5, + 30,410,8,30,10,30,12,30,413,9,30,1,31,1,31,1,31,5,31,418,8,31,10,31,12, + 31,421,9,31,1,32,1,32,1,33,1,33,1,33,3,33,428,8,33,1,34,1,34,1,34,1,34, + 1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,5,34,443,8,34,10,34,12,34, + 446,9,34,1,34,1,34,1,34,1,34,1,34,1,34,5,34,454,8,34,10,34,12,34,457,9, + 34,1,34,1,34,1,34,1,34,1,34,1,34,5,34,465,8,34,10,34,12,34,468,9,34,1,34, + 1,34,3,34,472,8,34,1,35,1,35,3,35,476,8,35,1,36,1,36,1,36,3,36,481,8,36, + 1,37,1,37,1,37,1,38,1,38,1,38,1,38,5,38,490,8,38,10,38,12,38,493,9,38,1, + 39,1,39,3,39,497,8,39,1,39,1,39,3,39,501,8,39,1,40,1,40,1,40,1,41,1,41, + 1,41,1,42,1,42,1,42,1,42,5,42,513,8,42,10,42,12,42,516,9,42,1,43,1,43,1, + 43,1,43,1,44,1,44,1,44,1,44,3,44,526,8,44,1,45,1,45,1,45,1,45,1,46,1,46, + 1,46,1,47,1,47,1,47,5,47,538,8,47,10,47,12,47,541,9,47,1,48,1,48,1,48,1, + 48,1,49,1,49,1,50,1,50,3,50,551,8,50,1,51,3,51,554,8,51,1,51,1,51,1,52, + 3,52,559,8,52,1,52,1,52,1,53,1,53,1,54,1,54,1,55,1,55,1,55,1,56,1,56,1, + 56,1,56,1,57,1,57,1,57,1,58,1,58,1,58,1,58,3,58,581,8,58,1,58,1,58,1,58, + 1,58,5,58,587,8,58,10,58,12,58,590,9,58,3,58,592,8,58,1,59,1,59,1,59,3, + 59,597,8,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,3,61, + 610,8,61,1,62,3,62,613,8,62,1,62,1,62,1,62,1,62,1,63,1,63,1,63,3,63,622, + 8,63,1,64,1,64,1,64,1,64,5,64,628,8,64,10,64,12,64,631,9,64,1,65,1,65,1, + 65,0,4,2,10,18,20,66,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36, + 38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84, + 86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124, + 126,128,130,0,9,1,0,64,65,1,0,66,68,2,0,30,30,81,81,1,0,72,73,2,0,35,35, + 40,40,2,0,43,43,46,46,2,0,42,42,56,56,2,0,57,57,59,63,1,0,22,24,660,0,132, + 1,0,0,0,2,135,1,0,0,0,4,152,1,0,0,0,6,172,1,0,0,0,8,174,1,0,0,0,10,206, + 1,0,0,0,12,233,1,0,0,0,14,235,1,0,0,0,16,244,1,0,0,0,18,250,1,0,0,0,20, + 271,1,0,0,0,22,281,1,0,0,0,24,296,1,0,0,0,26,298,1,0,0,0,28,300,1,0,0,0, + 30,303,1,0,0,0,32,314,1,0,0,0,34,318,1,0,0,0,36,333,1,0,0,0,38,337,1,0, + 0,0,40,339,1,0,0,0,42,343,1,0,0,0,44,345,1,0,0,0,46,354,1,0,0,0,48,358, + 1,0,0,0,50,374,1,0,0,0,52,377,1,0,0,0,54,385,1,0,0,0,56,393,1,0,0,0,58, + 398,1,0,0,0,60,406,1,0,0,0,62,414,1,0,0,0,64,422,1,0,0,0,66,427,1,0,0,0, + 68,471,1,0,0,0,70,475,1,0,0,0,72,480,1,0,0,0,74,482,1,0,0,0,76,485,1,0, + 0,0,78,494,1,0,0,0,80,502,1,0,0,0,82,505,1,0,0,0,84,508,1,0,0,0,86,517, + 1,0,0,0,88,521,1,0,0,0,90,527,1,0,0,0,92,531,1,0,0,0,94,534,1,0,0,0,96, + 542,1,0,0,0,98,546,1,0,0,0,100,550,1,0,0,0,102,553,1,0,0,0,104,558,1,0, + 0,0,106,562,1,0,0,0,108,564,1,0,0,0,110,566,1,0,0,0,112,569,1,0,0,0,114, + 573,1,0,0,0,116,576,1,0,0,0,118,596,1,0,0,0,120,600,1,0,0,0,122,605,1,0, + 0,0,124,612,1,0,0,0,126,618,1,0,0,0,128,623,1,0,0,0,130,632,1,0,0,0,132, + 133,3,2,1,0,133,134,5,0,0,1,134,1,1,0,0,0,135,136,6,1,-1,0,136,137,3,4, + 2,0,137,143,1,0,0,0,138,139,10,1,0,0,139,140,5,29,0,0,140,142,3,6,3,0,141, + 138,1,0,0,0,142,145,1,0,0,0,143,141,1,0,0,0,143,144,1,0,0,0,144,3,1,0,0, + 0,145,143,1,0,0,0,146,153,3,110,55,0,147,153,3,34,17,0,148,153,3,28,14, + 0,149,153,3,114,57,0,150,151,4,2,1,0,151,153,3,48,24,0,152,146,1,0,0,0, + 152,147,1,0,0,0,152,148,1,0,0,0,152,149,1,0,0,0,152,150,1,0,0,0,153,5,1, + 0,0,0,154,173,3,50,25,0,155,173,3,8,4,0,156,173,3,80,40,0,157,173,3,74, + 37,0,158,173,3,52,26,0,159,173,3,76,38,0,160,173,3,82,41,0,161,173,3,84, + 42,0,162,173,3,88,44,0,163,173,3,90,45,0,164,173,3,116,58,0,165,173,3,92, + 46,0,166,167,4,3,2,0,167,173,3,122,61,0,168,169,4,3,3,0,169,173,3,120,60, + 0,170,171,4,3,4,0,171,173,3,124,62,0,172,154,1,0,0,0,172,155,1,0,0,0,172, + 156,1,0,0,0,172,157,1,0,0,0,172,158,1,0,0,0,172,159,1,0,0,0,172,160,1,0, + 0,0,172,161,1,0,0,0,172,162,1,0,0,0,172,163,1,0,0,0,172,164,1,0,0,0,172, + 165,1,0,0,0,172,166,1,0,0,0,172,168,1,0,0,0,172,170,1,0,0,0,173,7,1,0,0, + 0,174,175,5,16,0,0,175,176,3,10,5,0,176,9,1,0,0,0,177,178,6,5,-1,0,178, + 179,5,49,0,0,179,207,3,10,5,8,180,207,3,16,8,0,181,207,3,12,6,0,182,184, + 3,16,8,0,183,185,5,49,0,0,184,183,1,0,0,0,184,185,1,0,0,0,185,186,1,0,0, + 0,186,187,5,44,0,0,187,188,5,48,0,0,188,193,3,16,8,0,189,190,5,39,0,0,190, + 192,3,16,8,0,191,189,1,0,0,0,192,195,1,0,0,0,193,191,1,0,0,0,193,194,1, + 0,0,0,194,196,1,0,0,0,195,193,1,0,0,0,196,197,5,55,0,0,197,207,1,0,0,0, + 198,199,3,16,8,0,199,201,5,45,0,0,200,202,5,49,0,0,201,200,1,0,0,0,201, + 202,1,0,0,0,202,203,1,0,0,0,203,204,5,50,0,0,204,207,1,0,0,0,205,207,3, + 14,7,0,206,177,1,0,0,0,206,180,1,0,0,0,206,181,1,0,0,0,206,182,1,0,0,0, + 206,198,1,0,0,0,206,205,1,0,0,0,207,216,1,0,0,0,208,209,10,5,0,0,209,210, + 5,34,0,0,210,215,3,10,5,6,211,212,10,4,0,0,212,213,5,52,0,0,213,215,3,10, + 5,5,214,208,1,0,0,0,214,211,1,0,0,0,215,218,1,0,0,0,216,214,1,0,0,0,216, + 217,1,0,0,0,217,11,1,0,0,0,218,216,1,0,0,0,219,221,3,16,8,0,220,222,5,49, + 0,0,221,220,1,0,0,0,221,222,1,0,0,0,222,223,1,0,0,0,223,224,5,47,0,0,224, + 225,3,106,53,0,225,234,1,0,0,0,226,228,3,16,8,0,227,229,5,49,0,0,228,227, + 1,0,0,0,228,229,1,0,0,0,229,230,1,0,0,0,230,231,5,54,0,0,231,232,3,106, + 53,0,232,234,1,0,0,0,233,219,1,0,0,0,233,226,1,0,0,0,234,13,1,0,0,0,235, + 236,3,58,29,0,236,237,5,38,0,0,237,238,3,68,34,0,238,15,1,0,0,0,239,245, + 3,18,9,0,240,241,3,18,9,0,241,242,3,108,54,0,242,243,3,18,9,0,243,245,1, + 0,0,0,244,239,1,0,0,0,244,240,1,0,0,0,245,17,1,0,0,0,246,247,6,9,-1,0,247, + 251,3,20,10,0,248,249,7,0,0,0,249,251,3,18,9,3,250,246,1,0,0,0,250,248, + 1,0,0,0,251,260,1,0,0,0,252,253,10,2,0,0,253,254,7,1,0,0,254,259,3,18,9, + 3,255,256,10,1,0,0,256,257,7,0,0,0,257,259,3,18,9,2,258,252,1,0,0,0,258, + 255,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,19,1,0, + 0,0,262,260,1,0,0,0,263,264,6,10,-1,0,264,272,3,68,34,0,265,272,3,58,29, + 0,266,272,3,22,11,0,267,268,5,48,0,0,268,269,3,10,5,0,269,270,5,55,0,0, + 270,272,1,0,0,0,271,263,1,0,0,0,271,265,1,0,0,0,271,266,1,0,0,0,271,267, + 1,0,0,0,272,278,1,0,0,0,273,274,10,1,0,0,274,275,5,37,0,0,275,277,3,26, + 13,0,276,273,1,0,0,0,277,280,1,0,0,0,278,276,1,0,0,0,278,279,1,0,0,0,279, + 21,1,0,0,0,280,278,1,0,0,0,281,282,3,24,12,0,282,292,5,48,0,0,283,293,5, + 66,0,0,284,289,3,10,5,0,285,286,5,39,0,0,286,288,3,10,5,0,287,285,1,0,0, + 0,288,291,1,0,0,0,289,287,1,0,0,0,289,290,1,0,0,0,290,293,1,0,0,0,291,289, + 1,0,0,0,292,283,1,0,0,0,292,284,1,0,0,0,292,293,1,0,0,0,293,294,1,0,0,0, + 294,295,5,55,0,0,295,23,1,0,0,0,296,297,3,72,36,0,297,25,1,0,0,0,298,299, + 3,64,32,0,299,27,1,0,0,0,300,301,5,12,0,0,301,302,3,30,15,0,302,29,1,0, + 0,0,303,308,3,32,16,0,304,305,5,39,0,0,305,307,3,32,16,0,306,304,1,0,0, + 0,307,310,1,0,0,0,308,306,1,0,0,0,308,309,1,0,0,0,309,31,1,0,0,0,310,308, + 1,0,0,0,311,312,3,58,29,0,312,313,5,36,0,0,313,315,1,0,0,0,314,311,1,0, + 0,0,314,315,1,0,0,0,315,316,1,0,0,0,316,317,3,10,5,0,317,33,1,0,0,0,318, + 319,5,6,0,0,319,324,3,36,18,0,320,321,5,39,0,0,321,323,3,36,18,0,322,320, + 1,0,0,0,323,326,1,0,0,0,324,322,1,0,0,0,324,325,1,0,0,0,325,328,1,0,0,0, + 326,324,1,0,0,0,327,329,3,42,21,0,328,327,1,0,0,0,328,329,1,0,0,0,329,35, + 1,0,0,0,330,331,3,38,19,0,331,332,5,38,0,0,332,334,1,0,0,0,333,330,1,0, + 0,0,333,334,1,0,0,0,334,335,1,0,0,0,335,336,3,40,20,0,336,37,1,0,0,0,337, + 338,5,81,0,0,338,39,1,0,0,0,339,340,7,2,0,0,340,41,1,0,0,0,341,344,3,44, + 22,0,342,344,3,46,23,0,343,341,1,0,0,0,343,342,1,0,0,0,344,43,1,0,0,0,345, + 346,5,80,0,0,346,351,5,81,0,0,347,348,5,39,0,0,348,350,5,81,0,0,349,347, + 1,0,0,0,350,353,1,0,0,0,351,349,1,0,0,0,351,352,1,0,0,0,352,45,1,0,0,0, + 353,351,1,0,0,0,354,355,5,70,0,0,355,356,3,44,22,0,356,357,5,71,0,0,357, + 47,1,0,0,0,358,359,5,19,0,0,359,364,3,36,18,0,360,361,5,39,0,0,361,363, + 3,36,18,0,362,360,1,0,0,0,363,366,1,0,0,0,364,362,1,0,0,0,364,365,1,0,0, + 0,365,368,1,0,0,0,366,364,1,0,0,0,367,369,3,54,27,0,368,367,1,0,0,0,368, + 369,1,0,0,0,369,372,1,0,0,0,370,371,5,33,0,0,371,373,3,30,15,0,372,370, + 1,0,0,0,372,373,1,0,0,0,373,49,1,0,0,0,374,375,5,4,0,0,375,376,3,30,15, + 0,376,51,1,0,0,0,377,379,5,15,0,0,378,380,3,54,27,0,379,378,1,0,0,0,379, + 380,1,0,0,0,380,383,1,0,0,0,381,382,5,33,0,0,382,384,3,30,15,0,383,381, + 1,0,0,0,383,384,1,0,0,0,384,53,1,0,0,0,385,390,3,56,28,0,386,387,5,39,0, + 0,387,389,3,56,28,0,388,386,1,0,0,0,389,392,1,0,0,0,390,388,1,0,0,0,390, + 391,1,0,0,0,391,55,1,0,0,0,392,390,1,0,0,0,393,396,3,32,16,0,394,395,5, + 16,0,0,395,397,3,10,5,0,396,394,1,0,0,0,396,397,1,0,0,0,397,57,1,0,0,0, + 398,403,3,72,36,0,399,400,5,41,0,0,400,402,3,72,36,0,401,399,1,0,0,0,402, + 405,1,0,0,0,403,401,1,0,0,0,403,404,1,0,0,0,404,59,1,0,0,0,405,403,1,0, + 0,0,406,411,3,66,33,0,407,408,5,41,0,0,408,410,3,66,33,0,409,407,1,0,0, + 0,410,413,1,0,0,0,411,409,1,0,0,0,411,412,1,0,0,0,412,61,1,0,0,0,413,411, + 1,0,0,0,414,419,3,60,30,0,415,416,5,39,0,0,416,418,3,60,30,0,417,415,1, + 0,0,0,418,421,1,0,0,0,419,417,1,0,0,0,419,420,1,0,0,0,420,63,1,0,0,0,421, + 419,1,0,0,0,422,423,7,3,0,0,423,65,1,0,0,0,424,428,5,85,0,0,425,426,4,33, + 10,0,426,428,3,70,35,0,427,424,1,0,0,0,427,425,1,0,0,0,428,67,1,0,0,0,429, + 472,5,50,0,0,430,431,3,104,52,0,431,432,5,72,0,0,432,472,1,0,0,0,433,472, + 3,102,51,0,434,472,3,104,52,0,435,472,3,98,49,0,436,472,3,70,35,0,437,472, + 3,106,53,0,438,439,5,70,0,0,439,444,3,100,50,0,440,441,5,39,0,0,441,443, + 3,100,50,0,442,440,1,0,0,0,443,446,1,0,0,0,444,442,1,0,0,0,444,445,1,0, + 0,0,445,447,1,0,0,0,446,444,1,0,0,0,447,448,5,71,0,0,448,472,1,0,0,0,449, + 450,5,70,0,0,450,455,3,98,49,0,451,452,5,39,0,0,452,454,3,98,49,0,453,451, + 1,0,0,0,454,457,1,0,0,0,455,453,1,0,0,0,455,456,1,0,0,0,456,458,1,0,0,0, + 457,455,1,0,0,0,458,459,5,71,0,0,459,472,1,0,0,0,460,461,5,70,0,0,461,466, + 3,106,53,0,462,463,5,39,0,0,463,465,3,106,53,0,464,462,1,0,0,0,465,468, + 1,0,0,0,466,464,1,0,0,0,466,467,1,0,0,0,467,469,1,0,0,0,468,466,1,0,0,0, + 469,470,5,71,0,0,470,472,1,0,0,0,471,429,1,0,0,0,471,430,1,0,0,0,471,433, + 1,0,0,0,471,434,1,0,0,0,471,435,1,0,0,0,471,436,1,0,0,0,471,437,1,0,0,0, + 471,438,1,0,0,0,471,449,1,0,0,0,471,460,1,0,0,0,472,69,1,0,0,0,473,476, + 5,53,0,0,474,476,5,69,0,0,475,473,1,0,0,0,475,474,1,0,0,0,476,71,1,0,0, + 0,477,481,3,64,32,0,478,479,4,36,11,0,479,481,3,70,35,0,480,477,1,0,0,0, + 480,478,1,0,0,0,481,73,1,0,0,0,482,483,5,9,0,0,483,484,5,31,0,0,484,75, + 1,0,0,0,485,486,5,14,0,0,486,491,3,78,39,0,487,488,5,39,0,0,488,490,3,78, + 39,0,489,487,1,0,0,0,490,493,1,0,0,0,491,489,1,0,0,0,491,492,1,0,0,0,492, + 77,1,0,0,0,493,491,1,0,0,0,494,496,3,10,5,0,495,497,7,4,0,0,496,495,1,0, + 0,0,496,497,1,0,0,0,497,500,1,0,0,0,498,499,5,51,0,0,499,501,7,5,0,0,500, + 498,1,0,0,0,500,501,1,0,0,0,501,79,1,0,0,0,502,503,5,8,0,0,503,504,3,62, + 31,0,504,81,1,0,0,0,505,506,5,2,0,0,506,507,3,62,31,0,507,83,1,0,0,0,508, + 509,5,11,0,0,509,514,3,86,43,0,510,511,5,39,0,0,511,513,3,86,43,0,512,510, + 1,0,0,0,513,516,1,0,0,0,514,512,1,0,0,0,514,515,1,0,0,0,515,85,1,0,0,0, + 516,514,1,0,0,0,517,518,3,60,30,0,518,519,5,89,0,0,519,520,3,60,30,0,520, + 87,1,0,0,0,521,522,5,1,0,0,522,523,3,20,10,0,523,525,3,106,53,0,524,526, + 3,94,47,0,525,524,1,0,0,0,525,526,1,0,0,0,526,89,1,0,0,0,527,528,5,7,0, + 0,528,529,3,20,10,0,529,530,3,106,53,0,530,91,1,0,0,0,531,532,5,10,0,0, + 532,533,3,58,29,0,533,93,1,0,0,0,534,539,3,96,48,0,535,536,5,39,0,0,536, + 538,3,96,48,0,537,535,1,0,0,0,538,541,1,0,0,0,539,537,1,0,0,0,539,540,1, + 0,0,0,540,95,1,0,0,0,541,539,1,0,0,0,542,543,3,64,32,0,543,544,5,36,0,0, + 544,545,3,68,34,0,545,97,1,0,0,0,546,547,7,6,0,0,547,99,1,0,0,0,548,551, + 3,102,51,0,549,551,3,104,52,0,550,548,1,0,0,0,550,549,1,0,0,0,551,101,1, + 0,0,0,552,554,7,0,0,0,553,552,1,0,0,0,553,554,1,0,0,0,554,555,1,0,0,0,555, + 556,5,32,0,0,556,103,1,0,0,0,557,559,7,0,0,0,558,557,1,0,0,0,558,559,1, + 0,0,0,559,560,1,0,0,0,560,561,5,31,0,0,561,105,1,0,0,0,562,563,5,30,0,0, + 563,107,1,0,0,0,564,565,7,7,0,0,565,109,1,0,0,0,566,567,5,5,0,0,567,568, + 3,112,56,0,568,111,1,0,0,0,569,570,5,70,0,0,570,571,3,2,1,0,571,572,5,71, + 0,0,572,113,1,0,0,0,573,574,5,13,0,0,574,575,5,105,0,0,575,115,1,0,0,0, + 576,577,5,3,0,0,577,580,5,95,0,0,578,579,5,93,0,0,579,581,3,60,30,0,580, + 578,1,0,0,0,580,581,1,0,0,0,581,591,1,0,0,0,582,583,5,94,0,0,583,588,3, + 118,59,0,584,585,5,39,0,0,585,587,3,118,59,0,586,584,1,0,0,0,587,590,1, + 0,0,0,588,586,1,0,0,0,588,589,1,0,0,0,589,592,1,0,0,0,590,588,1,0,0,0,591, + 582,1,0,0,0,591,592,1,0,0,0,592,117,1,0,0,0,593,594,3,60,30,0,594,595,5, + 36,0,0,595,597,1,0,0,0,596,593,1,0,0,0,596,597,1,0,0,0,597,598,1,0,0,0, + 598,599,3,60,30,0,599,119,1,0,0,0,600,601,5,18,0,0,601,602,3,36,18,0,602, + 603,5,93,0,0,603,604,3,62,31,0,604,121,1,0,0,0,605,606,5,17,0,0,606,609, + 3,54,27,0,607,608,5,33,0,0,608,610,3,30,15,0,609,607,1,0,0,0,609,610,1, + 0,0,0,610,123,1,0,0,0,611,613,7,8,0,0,612,611,1,0,0,0,612,613,1,0,0,0,613, + 614,1,0,0,0,614,615,5,20,0,0,615,616,3,126,63,0,616,617,3,128,64,0,617, + 125,1,0,0,0,618,621,3,64,32,0,619,620,5,89,0,0,620,622,3,64,32,0,621,619, + 1,0,0,0,621,622,1,0,0,0,622,127,1,0,0,0,623,624,5,93,0,0,624,629,3,130, + 65,0,625,626,5,39,0,0,626,628,3,130,65,0,627,625,1,0,0,0,628,631,1,0,0, + 0,629,627,1,0,0,0,629,630,1,0,0,0,630,129,1,0,0,0,631,629,1,0,0,0,632,633, + 3,16,8,0,633,131,1,0,0,0,61,143,152,172,184,193,201,206,214,216,221,228, + 233,244,250,258,260,271,278,289,292,308,314,324,328,333,343,351,364,368, + 372,379,383,390,396,403,411,419,427,444,455,466,471,475,480,491,496,500, + 514,525,539,550,553,558,580,588,591,596,609,612,621,629]; private static __ATN: ATN; public static get _ATN(): ATN { @@ -3833,6 +4040,9 @@ export class ProcessingCommandContext extends ParserRuleContext { public lookupCommand(): LookupCommandContext { return this.getTypedRuleContext(LookupCommandContext, 0) as LookupCommandContext; } + public joinCommand(): JoinCommandContext { + return this.getTypedRuleContext(JoinCommandContext, 0) as JoinCommandContext; + } public get ruleIndex(): number { return esql_parser.RULE_processingCommand; } @@ -6278,3 +6488,135 @@ export class InlinestatsCommandContext extends ParserRuleContext { } } } + + +export class JoinCommandContext extends ParserRuleContext { + public _type_!: Token; + constructor(parser?: esql_parser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public DEV_JOIN(): TerminalNode { + return this.getToken(esql_parser.DEV_JOIN, 0); + } + public joinTarget(): JoinTargetContext { + return this.getTypedRuleContext(JoinTargetContext, 0) as JoinTargetContext; + } + public joinCondition(): JoinConditionContext { + return this.getTypedRuleContext(JoinConditionContext, 0) as JoinConditionContext; + } + public DEV_JOIN_LOOKUP(): TerminalNode { + return this.getToken(esql_parser.DEV_JOIN_LOOKUP, 0); + } + public DEV_JOIN_LEFT(): TerminalNode { + return this.getToken(esql_parser.DEV_JOIN_LEFT, 0); + } + public DEV_JOIN_RIGHT(): TerminalNode { + return this.getToken(esql_parser.DEV_JOIN_RIGHT, 0); + } + public get ruleIndex(): number { + return esql_parser.RULE_joinCommand; + } + public enterRule(listener: esql_parserListener): void { + if(listener.enterJoinCommand) { + listener.enterJoinCommand(this); + } + } + public exitRule(listener: esql_parserListener): void { + if(listener.exitJoinCommand) { + listener.exitJoinCommand(this); + } + } +} + + +export class JoinTargetContext extends ParserRuleContext { + public _index!: IdentifierContext; + public _alias!: IdentifierContext; + constructor(parser?: esql_parser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public identifier_list(): IdentifierContext[] { + return this.getTypedRuleContexts(IdentifierContext) as IdentifierContext[]; + } + public identifier(i: number): IdentifierContext { + return this.getTypedRuleContext(IdentifierContext, i) as IdentifierContext; + } + public AS(): TerminalNode { + return this.getToken(esql_parser.AS, 0); + } + public get ruleIndex(): number { + return esql_parser.RULE_joinTarget; + } + public enterRule(listener: esql_parserListener): void { + if(listener.enterJoinTarget) { + listener.enterJoinTarget(this); + } + } + public exitRule(listener: esql_parserListener): void { + if(listener.exitJoinTarget) { + listener.exitJoinTarget(this); + } + } +} + + +export class JoinConditionContext extends ParserRuleContext { + constructor(parser?: esql_parser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public ON(): TerminalNode { + return this.getToken(esql_parser.ON, 0); + } + public joinPredicate_list(): JoinPredicateContext[] { + return this.getTypedRuleContexts(JoinPredicateContext) as JoinPredicateContext[]; + } + public joinPredicate(i: number): JoinPredicateContext { + return this.getTypedRuleContext(JoinPredicateContext, i) as JoinPredicateContext; + } + public COMMA_list(): TerminalNode[] { + return this.getTokens(esql_parser.COMMA); + } + public COMMA(i: number): TerminalNode { + return this.getToken(esql_parser.COMMA, i); + } + public get ruleIndex(): number { + return esql_parser.RULE_joinCondition; + } + public enterRule(listener: esql_parserListener): void { + if(listener.enterJoinCondition) { + listener.enterJoinCondition(this); + } + } + public exitRule(listener: esql_parserListener): void { + if(listener.exitJoinCondition) { + listener.exitJoinCondition(this); + } + } +} + + +export class JoinPredicateContext extends ParserRuleContext { + constructor(parser?: esql_parser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public valueExpression(): ValueExpressionContext { + return this.getTypedRuleContext(ValueExpressionContext, 0) as ValueExpressionContext; + } + public get ruleIndex(): number { + return esql_parser.RULE_joinPredicate; + } + public enterRule(listener: esql_parserListener): void { + if(listener.enterJoinPredicate) { + listener.enterJoinPredicate(this); + } + } + public exitRule(listener: esql_parserListener): void { + if(listener.exitJoinPredicate) { + listener.exitJoinPredicate(this); + } + } +} diff --git a/packages/kbn-esql-ast/src/antlr/esql_parser_listener.ts b/packages/kbn-esql-ast/src/antlr/esql_parser_listener.ts index 576418862be01..d206b099dd588 100644 --- a/packages/kbn-esql-ast/src/antlr/esql_parser_listener.ts +++ b/packages/kbn-esql-ast/src/antlr/esql_parser_listener.ts @@ -98,6 +98,10 @@ import { EnrichCommandContext } from "./esql_parser.js"; import { EnrichWithClauseContext } from "./esql_parser.js"; import { LookupCommandContext } from "./esql_parser.js"; import { InlinestatsCommandContext } from "./esql_parser.js"; +import { JoinCommandContext } from "./esql_parser.js"; +import { JoinTargetContext } from "./esql_parser.js"; +import { JoinConditionContext } from "./esql_parser.js"; +import { JoinPredicateContext } from "./esql_parser.js"; /** @@ -1031,5 +1035,45 @@ export default class esql_parserListener extends ParseTreeListener { * @param ctx the parse tree */ exitInlinestatsCommand?: (ctx: InlinestatsCommandContext) => void; + /** + * Enter a parse tree produced by `esql_parser.joinCommand`. + * @param ctx the parse tree + */ + enterJoinCommand?: (ctx: JoinCommandContext) => void; + /** + * Exit a parse tree produced by `esql_parser.joinCommand`. + * @param ctx the parse tree + */ + exitJoinCommand?: (ctx: JoinCommandContext) => void; + /** + * Enter a parse tree produced by `esql_parser.joinTarget`. + * @param ctx the parse tree + */ + enterJoinTarget?: (ctx: JoinTargetContext) => void; + /** + * Exit a parse tree produced by `esql_parser.joinTarget`. + * @param ctx the parse tree + */ + exitJoinTarget?: (ctx: JoinTargetContext) => void; + /** + * Enter a parse tree produced by `esql_parser.joinCondition`. + * @param ctx the parse tree + */ + enterJoinCondition?: (ctx: JoinConditionContext) => void; + /** + * Exit a parse tree produced by `esql_parser.joinCondition`. + * @param ctx the parse tree + */ + exitJoinCondition?: (ctx: JoinConditionContext) => void; + /** + * Enter a parse tree produced by `esql_parser.joinPredicate`. + * @param ctx the parse tree + */ + enterJoinPredicate?: (ctx: JoinPredicateContext) => void; + /** + * Exit a parse tree produced by `esql_parser.joinPredicate`. + * @param ctx the parse tree + */ + exitJoinPredicate?: (ctx: JoinPredicateContext) => void; } diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/generated/aggregation_functions.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/generated/aggregation_functions.ts index 37dfe8de5822f..4bfdbb6a16395 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/generated/aggregation_functions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/generated/aggregation_functions.ts @@ -1341,6 +1341,56 @@ const stCentroidAggDefinition: FunctionDefinition = { examples: ['FROM airports\n| STATS centroid=ST_CENTROID_AGG(location)'], }; +// Do not edit this manually... generated by scripts/generate_function_definitions.ts +const stdDevDefinition: FunctionDefinition = { + type: 'agg', + name: 'std_dev', + description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.std_dev', { + defaultMessage: 'The standard deviation of a numeric field.', + }), + preview: false, + alias: undefined, + signatures: [ + { + params: [ + { + name: 'number', + type: 'double', + optional: false, + }, + ], + returnType: 'double', + }, + { + params: [ + { + name: 'number', + type: 'integer', + optional: false, + }, + ], + returnType: 'double', + }, + { + params: [ + { + name: 'number', + type: 'long', + optional: false, + }, + ], + returnType: 'double', + }, + ], + supportedCommands: ['stats', 'inlinestats', 'metrics'], + supportedOptions: undefined, + validate: undefined, + examples: [ + 'FROM employees\n| STATS STD_DEV(height)', + 'FROM employees\n| STATS stddev_salary_change = STD_DEV(MV_MAX(salary_change))', + ], +}; + // Do not edit this manually... generated by scripts/generate_function_definitions.ts const sumDefinition: FunctionDefinition = { type: 'agg', @@ -1871,6 +1921,7 @@ export const aggregationFunctionDefinitions = [ minDefinition, percentileDefinition, stCentroidAggDefinition, + stdDevDefinition, sumDefinition, topDefinition, valuesDefinition, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index fee9f90f38c93..2e6b4a085656f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -2203,7 +2203,7 @@ "warning": [] }, { - "query": "ROW a=1::LONG | LOOKUP t ON a", + "query": "ROW a=1::LONG | LOOKUP JOIN t ON a", "error": [], "warning": [] }, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index 03102474f6314..442a2299d8abe 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -506,7 +506,7 @@ describe('validation logic', () => { }); describe('lookup', () => { - testErrorsAndWarnings('ROW a=1::LONG | LOOKUP t ON a', []); + testErrorsAndWarnings('ROW a=1::LONG | LOOKUP JOIN t ON a', []); }); describe('keep', () => { diff --git a/packages/kbn-generate-csv/src/generate_csv.test.ts b/packages/kbn-generate-csv/src/generate_csv.test.ts index f39cf51352a58..e2999b63088d3 100644 --- a/packages/kbn-generate-csv/src/generate_csv.test.ts +++ b/packages/kbn-generate-csv/src/generate_csv.test.ts @@ -47,7 +47,6 @@ const getMockConfig = (opts: Partial = {}): CsvConfigType => ({ maxSizeBytes: 180000, useByteOrderMarkEncoding: false, scroll: { size: 500, duration: '30s', strategy: 'pit' }, - enablePanelActionDownload: false, maxConcurrentShardRequests: 5, ...opts, }); diff --git a/packages/kbn-generate-csv/src/generate_csv_esql.test.ts b/packages/kbn-generate-csv/src/generate_csv_esql.test.ts index 9ae0b2b711c19..d2ee8e8345438 100644 --- a/packages/kbn-generate-csv/src/generate_csv_esql.test.ts +++ b/packages/kbn-generate-csv/src/generate_csv_esql.test.ts @@ -98,7 +98,6 @@ describe('CsvESQLGenerator', () => { maxSizeBytes: 180000, useByteOrderMarkEncoding: false, scroll: { size: 500, duration: '30s', strategy: 'pit' }, - enablePanelActionDownload: false, maxConcurrentShardRequests: 5, }; @@ -569,7 +568,6 @@ describe('CsvESQLGenerator', () => { maxSizeBytes: 180000, useByteOrderMarkEncoding: false, scroll: { size: 500, duration: '30s', strategy: 'pit' }, - enablePanelActionDownload: false, maxConcurrentShardRequests: 5, }; mockSearchResponse({ diff --git a/packages/kbn-generate-csv/src/lib/get_export_settings.test.ts b/packages/kbn-generate-csv/src/lib/get_export_settings.test.ts index 05a321aa1a255..f1c73680a8b9d 100644 --- a/packages/kbn-generate-csv/src/lib/get_export_settings.test.ts +++ b/packages/kbn-generate-csv/src/lib/get_export_settings.test.ts @@ -39,7 +39,6 @@ describe('getExportSettings', () => { scroll: { size: 500, duration: '30s', strategy: 'pit' }, useByteOrderMarkEncoding: false, maxConcurrentShardRequests: 5, - enablePanelActionDownload: false, }; taskInstanceFields = { startedAt: null, retryAt: null }; diff --git a/packages/kbn-grouping/src/hooks/state/reducer.test.ts b/packages/kbn-grouping/src/hooks/state/reducer.test.ts index c056565b7bf11..7d00d64eadd23 100644 --- a/packages/kbn-grouping/src/hooks/state/reducer.test.ts +++ b/packages/kbn-grouping/src/hooks/state/reducer.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useReducer } from 'react'; import { groupActions, groupsReducerWithStorage, initialState } from '.'; import { defaultGroup, LOCAL_STORAGE_GROUPING_KEY } from '../..'; diff --git a/packages/kbn-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-grouping/src/hooks/use_get_group_selector.test.tsx index 312ccde33e32a..d29e1b63f1ea9 100644 --- a/packages/kbn-grouping/src/hooks/use_get_group_selector.test.tsx +++ b/packages/kbn-grouping/src/hooks/use_get_group_selector.test.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useGetGroupSelector, useGetGroupSelectorStateless } from './use_get_group_selector'; import { initialState } from './state'; diff --git a/packages/kbn-grouping/src/hooks/use_grouping.test.tsx b/packages/kbn-grouping/src/hooks/use_grouping.test.tsx index 22957548de314..834db5acaa39f 100644 --- a/packages/kbn-grouping/src/hooks/use_grouping.test.tsx +++ b/packages/kbn-grouping/src/hooks/use_grouping.test.tsx @@ -8,9 +8,8 @@ */ import React from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { render } from '@testing-library/react'; +import { render, waitFor, renderHook } from '@testing-library/react'; import { useGrouping } from './use_grouping'; @@ -46,92 +45,86 @@ const groupingArgs = { describe('useGrouping', () => { it('Renders child component without grouping table wrapper when no group is selected', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGrouping(defaultArgs)); - await waitForNextUpdate(); - await waitForNextUpdate(); - const { getByTestId, queryByTestId } = render( - - {result.current.getGrouping({ - ...groupingArgs, - data: { - groupsCount: { - value: 9, - }, - groupByFields: { - buckets: [ - { - key: ['critical hosts', 'description'], - key_as_string: 'critical hosts|description', - doc_count: 3, - unitsCount: { - value: 3, - }, + const { result } = renderHook(() => useGrouping(defaultArgs)); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getByTestId, queryByTestId } = render( + + {result.current.getGrouping({ + ...groupingArgs, + data: { + groupsCount: { + value: 9, + }, + groupByFields: { + buckets: [ + { + key: ['critical hosts', 'description'], + key_as_string: 'critical hosts|description', + doc_count: 3, + unitsCount: { + value: 3, }, - ], - }, - unitsCount: { - value: 18, - }, + }, + ], + }, + unitsCount: { + value: 18, }, - renderChildComponent: () =>

{'hello'}

, - selectedGroup: 'none', - })} -
- ); + }, + renderChildComponent: () =>

{'hello'}

, + selectedGroup: 'none', + })} +
+ ); - expect(getByTestId('innerTable')).toBeInTheDocument(); - expect(queryByTestId('grouping-table')).not.toBeInTheDocument(); - }); + expect(getByTestId('innerTable')).toBeInTheDocument(); + expect(queryByTestId('grouping-table')).not.toBeInTheDocument(); }); it('Renders child component with grouping table wrapper when group is selected', async () => { - await act(async () => { - const getItem = jest.spyOn(window.localStorage.__proto__, 'getItem'); - getItem.mockReturnValue( - JSON.stringify({ - 'test-table': { - itemsPerPageOptions: [10, 25, 50, 100], - itemsPerPage: 25, - activeGroup: 'kibana.alert.rule.name', - options: defaultGroupingOptions, - }, - }) - ); + const getItem = jest.spyOn(window.localStorage.__proto__, 'getItem'); + getItem.mockReturnValue( + JSON.stringify({ + 'test-table': { + itemsPerPageOptions: [10, 25, 50, 100], + itemsPerPage: 25, + activeGroup: 'kibana.alert.rule.name', + options: defaultGroupingOptions, + }, + }) + ); - const { result, waitForNextUpdate } = renderHook(() => useGrouping(defaultArgs)); - await waitForNextUpdate(); - await waitForNextUpdate(); - const { getByTestId } = render( - - {result.current.getGrouping({ - ...groupingArgs, - data: { - groupsCount: { - value: 9, - }, - groupByFields: { - buckets: [ - { - key: ['critical hosts', 'description'], - key_as_string: 'critical hosts|description', - doc_count: 3, - unitsCount: { - value: 3, - }, + const { result } = renderHook(() => useGrouping(defaultArgs)); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getByTestId } = render( + + {result.current.getGrouping({ + ...groupingArgs, + data: { + groupsCount: { + value: 9, + }, + groupByFields: { + buckets: [ + { + key: ['critical hosts', 'description'], + key_as_string: 'critical hosts|description', + doc_count: 3, + unitsCount: { + value: 3, }, - ], - }, - unitsCount: { - value: 18, - }, + }, + ], + }, + unitsCount: { + value: 18, }, - renderChildComponent: jest.fn(), - selectedGroup: 'test', - })} - - ); + }, + renderChildComponent: jest.fn(), + selectedGroup: 'test', + })} + + ); - expect(getByTestId('grouping-table')).toBeInTheDocument(); - }); + expect(getByTestId('grouping-table')).toBeInTheDocument(); }); }); diff --git a/packages/kbn-language-documentation/src/sections/generated/aggregation_functions.tsx b/packages/kbn-language-documentation/src/sections/generated/aggregation_functions.tsx index 6fe761e489c04..96cbe74ab208f 100644 --- a/packages/kbn-language-documentation/src/sections/generated/aggregation_functions.tsx +++ b/packages/kbn-language-documentation/src/sections/generated/aggregation_functions.tsx @@ -318,6 +318,40 @@ export const functions = { FROM airports | STATS centroid=ST_CENTROID_AGG(location) \`\`\` + `, + description: + 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', + ignoreTag: true, + } + )} + /> + ), + }, + // Do not edit manually... automatically generated by scripts/generate_esql_docs.ts + { + label: i18n.translate('languageDocumentation.documentationESQL.std_dev', { + defaultMessage: 'STD_DEV', + }), + preview: false, + description: ( + + + ### STD_DEV + The standard deviation of a numeric field. + + \`\`\` + FROM employees + | STATS STD_DEV(height) + \`\`\` `, description: 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', diff --git a/packages/kbn-management/settings/components/field_input/input/number_input.test.tsx b/packages/kbn-management/settings/components/field_input/input/number_input.test.tsx index 4d3b5ef9a405b..7257a5395f2fb 100644 --- a/packages/kbn-management/settings/components/field_input/input/number_input.test.tsx +++ b/packages/kbn-management/settings/components/field_input/input/number_input.test.tsx @@ -85,11 +85,11 @@ describe('NumberInput', () => { expect(input).toBeDisabled(); }); - it('recovers if value is null', () => { + it('recovers if value is null', async () => { const { getByTestId } = render( wrap() ); - const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`); - waitFor(() => expect(input).toHaveValue(undefined)); + + await waitFor(() => expect(getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`)).toHaveValue(null)); }); }); diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts index c2a200e650804..ba70ee4c2f061 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts @@ -95,6 +95,7 @@ describe('ESQL Theme', () => { 'setting_ws', 'metrics_ws', 'closing_metrics_ws', + 'join_ws', ]; // First, check that every valid exception is actually valid diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.ts index 07a4d723b63e8..d8223df99bbd1 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.ts @@ -74,9 +74,15 @@ export const buildESQLTheme = ({ 'as', 'limit', 'dev_lookup', + 'dev_join_lookup', + 'dev_join', + 'dev_join_full', + 'dev_join_left', + 'dev_join_right', 'null', 'enrich', 'on', + 'using', 'with', 'asc', 'desc', @@ -138,6 +144,8 @@ export const buildESQLTheme = ({ 'lookup_multiline_comment', 'lookup_field_line_comment', 'lookup_field_multiline_comment', + 'join_line_comment', + 'join_multiline_comment', 'show_line_comment', 'show_multiline_comment', 'setting', diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index e1e9b6aa81898..caa5c3b53d193 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -42,6 +42,7 @@ pageLoadAssetSize: embeddableEnhanced: 22107 enterpriseSearch: 66810 entityManager: 17175 + entityManagerApp: 20378 esql: 37000 esqlDataGrid: 24582 esUiShared: 326654 @@ -143,6 +144,7 @@ pageLoadAssetSize: searchHomepage: 19831 searchIndices: 20519 searchInferenceEndpoints: 20470 + searchNavigation: 19233 searchNotebooks: 18942 searchPlayground: 19325 searchprofiler: 67080 @@ -161,6 +163,7 @@ pageLoadAssetSize: stackAlerts: 58316 stackConnectors: 67227 streams: 16742 + streamsApp: 20537 synthetics: 55971 telemetry: 51957 telemetryManagementSection: 38586 diff --git a/packages/kbn-react-hooks/src/use_boolean/use_boolean.test.ts b/packages/kbn-react-hooks/src/use_boolean/use_boolean.test.ts index 8d5583e926bac..9711d54823d35 100644 --- a/packages/kbn-react-hooks/src/use_boolean/use_boolean.test.ts +++ b/packages/kbn-react-hooks/src/use_boolean/use_boolean.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, cleanup, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act, cleanup } from '@testing-library/react'; import { useBoolean } from './use_boolean'; diff --git a/packages/kbn-repo-packages/index.js b/packages/kbn-repo-packages/index.js index b8784b3ef4b2f..067d75b6e1697 100644 --- a/packages/kbn-repo-packages/index.js +++ b/packages/kbn-repo-packages/index.js @@ -35,6 +35,7 @@ const { parseKbnImportReq } = require('./modern/parse_kbn_import_req'); const { getRepoRels, getRepoRelsSync } = require('./modern/get_repo_rels'); const Jsonc = require('./utils/jsonc'); const { getPluginPackagesFilter, getPluginSearchPaths } = require('./modern/plugins'); +const { readPackageJson } = require('./modern/parse_package_json'); module.exports = { Package, @@ -52,4 +53,5 @@ module.exports = { parseKbnImportReq, getRepoRels, getRepoRelsSync, + readPackageJson, }; diff --git a/packages/kbn-repo-source-classifier/src/config.ts b/packages/kbn-repo-source-classifier/src/config.ts index e6f8465a54ad5..08240db981694 100644 --- a/packages/kbn-repo-source-classifier/src/config.ts +++ b/packages/kbn-repo-source-classifier/src/config.ts @@ -58,5 +58,6 @@ export const TEST_DIR = new Set([ 'storybook', '.storybook', 'integration_tests', + 'ui_tests', ...RANDOM_TEST_FILE_NAMES, ]); diff --git a/packages/kbn-reporting/server/__snapshots__/config_schema.test.ts.snap b/packages/kbn-reporting/server/__snapshots__/config_schema.test.ts.snap index fecc985c76f34..d52630ab6820c 100644 --- a/packages/kbn-reporting/server/__snapshots__/config_schema.test.ts.snap +++ b/packages/kbn-reporting/server/__snapshots__/config_schema.test.ts.snap @@ -7,7 +7,6 @@ Object { }, "csv": Object { "checkForFormulas": true, - "enablePanelActionDownload": false, "escapeFormulaValues": false, "maxConcurrentShardRequests": 5, "maxSizeBytes": ByteSizeValue { @@ -70,7 +69,6 @@ Object { }, "csv": Object { "checkForFormulas": true, - "enablePanelActionDownload": false, "escapeFormulaValues": false, "maxConcurrentShardRequests": 5, "maxSizeBytes": ByteSizeValue { diff --git a/packages/kbn-reporting/server/config_schema.ts b/packages/kbn-reporting/server/config_schema.ts index a4117341d27ca..e14bc30ab56f7 100644 --- a/packages/kbn-reporting/server/config_schema.ts +++ b/packages/kbn-reporting/server/config_schema.ts @@ -60,7 +60,7 @@ const CaptureSchema = schema.object({ const CsvSchema = schema.object({ checkForFormulas: schema.boolean({ defaultValue: true }), escapeFormulaValues: schema.boolean({ defaultValue: false }), - enablePanelActionDownload: schema.boolean({ defaultValue: false }), // unused as of 9.0 + enablePanelActionDownload: schema.maybe(schema.boolean({ defaultValue: false })), // unused as of 9.0 maxSizeBytes: schema.oneOf([schema.number(), schema.byteSize()], { defaultValue: ByteSizeValue.parse('250mb'), }), diff --git a/packages/kbn-scout/README.md b/packages/kbn-scout/README.md new file mode 100644 index 0000000000000..4449bdf966200 --- /dev/null +++ b/packages/kbn-scout/README.md @@ -0,0 +1,9 @@ +# @kbn/scout + +The package is designed to streamline the setup and execution of Playwright tests for Kibana. It consolidates server management and testing capabilities by wrapping both the Kibana/Elasticsearch server launcher and the Playwright test runner. It includes: + + - core test and worker-scoped fixtures for reliable setup across test suites + - page objects combined into the fixture for for core Kibana apps UI interactions + - configurations for seamless test execution in both local and CI environments + +This package aims to simplify test setup and enhance modularity, making it easier to create, run, and maintain deployment-agnostic tests, that are located in the plugin they actually test. diff --git a/packages/kbn-scout/index.ts b/packages/kbn-scout/index.ts new file mode 100644 index 0000000000000..2cbf98d96a8e0 --- /dev/null +++ b/packages/kbn-scout/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { startServersCli, runTestsCli } from './src/cli'; +export { expect, test, createPlaywrightConfig, createLazyPageObject } from './src/playwright'; +export type { + ScoutPage, + ScoutPlaywrightOptions, + ScoutTestOptions, + PageObjects, + ScoutTestFixtures, + ScoutWorkerFixtures, +} from './src/playwright'; diff --git a/packages/kbn-scout/jest.config.js b/packages/kbn-scout/jest.config.js new file mode 100644 index 0000000000000..0e1493f115c12 --- /dev/null +++ b/packages/kbn-scout/jest.config.js @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-scout'], +}; diff --git a/packages/kbn-scout/kibana.jsonc b/packages/kbn-scout/kibana.jsonc new file mode 100644 index 0000000000000..c35c71e9793d8 --- /dev/null +++ b/packages/kbn-scout/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "type": "test-helper", + "id": "@kbn/scout", + "owner": "@elastic/appex-qa", + "devOnly": true +} diff --git a/packages/kbn-scout/package.json b/packages/kbn-scout/package.json new file mode 100644 index 0000000000000..fb362e66af2e9 --- /dev/null +++ b/packages/kbn-scout/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/scout", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/packages/kbn-scout/src/cli/index.ts b/packages/kbn-scout/src/cli/index.ts new file mode 100644 index 0000000000000..f30b384f351d9 --- /dev/null +++ b/packages/kbn-scout/src/cli/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { runTestsCli } from './run_tests_cli'; +export { startServersCli } from './start_servers_cli'; diff --git a/packages/kbn-scout/src/cli/run_tests_cli.ts b/packages/kbn-scout/src/cli/run_tests_cli.ts new file mode 100644 index 0000000000000..913f09a310a63 --- /dev/null +++ b/packages/kbn-scout/src/cli/run_tests_cli.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { run } from '@kbn/dev-cli-runner'; +import { initLogsDir } from '@kbn/test'; +import { TEST_FLAG_OPTIONS, parseTestFlags, runTests } from '../playwright/runner'; + +/** + * Start servers and run the tests + */ +export function runTestsCli() { + run( + async ({ flagsReader, log }) => { + const options = await parseTestFlags(flagsReader); + + if (options.logsDir) { + initLogsDir(log, options.logsDir); + } + + await runTests(log, options); + }, + { + description: `Run Scout UI Tests`, + usage: ` + Usage: + node scripts/scout_test --help + node scripts/scout_test --stateful --config + node scripts/scout_test --serverless=es --headed --config + `, + flags: TEST_FLAG_OPTIONS, + } + ); +} diff --git a/packages/kbn-scout/src/cli/start_servers_cli.ts b/packages/kbn-scout/src/cli/start_servers_cli.ts new file mode 100644 index 0000000000000..3006f87f5ba57 --- /dev/null +++ b/packages/kbn-scout/src/cli/start_servers_cli.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { run } from '@kbn/dev-cli-runner'; + +import { initLogsDir } from '@kbn/test'; + +import { startServers, parseServerFlags, SERVER_FLAG_OPTIONS } from '../servers'; + +/** + * Start servers + */ +export function startServersCli() { + run( + async ({ flagsReader: flags, log }) => { + const options = parseServerFlags(flags); + + if (options.logsDir) { + initLogsDir(log, options.logsDir); + } + + await startServers(log, options); + }, + { + flags: SERVER_FLAG_OPTIONS, + } + ); +} diff --git a/packages/kbn-scout/src/common/constants.ts b/packages/kbn-scout/src/common/constants.ts new file mode 100644 index 0000000000000..bf5c6fb181cd7 --- /dev/null +++ b/packages/kbn-scout/src/common/constants.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Role } from '@kbn/test/src/auth/types'; + +export const PROJECT_DEFAULT_ROLES = new Map([ + ['es', 'developer'], + ['security', 'editor'], + ['oblt', 'editor'], +]); diff --git a/packages/kbn-scout/src/common/index.ts b/packages/kbn-scout/src/common/index.ts new file mode 100644 index 0000000000000..7ff3c1ea52358 --- /dev/null +++ b/packages/kbn-scout/src/common/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './services'; +export * from './constants'; +export * from './utils'; diff --git a/packages/kbn-scout/src/common/services/clients.ts b/packages/kbn-scout/src/common/services/clients.ts new file mode 100644 index 0000000000000..3a0dcf8bfe320 --- /dev/null +++ b/packages/kbn-scout/src/common/services/clients.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { KbnClient, createEsClientForTesting } from '@kbn/test'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { ScoutServerConfig } from '../../types'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +interface ClientOptions { + serviceName: string; + url: string; + username: string; + password: string; + log: ToolingLog; +} + +function createClientUrlWithAuth({ serviceName, url, username, password, log }: ClientOptions) { + const clientUrl = new URL(url); + clientUrl.username = username; + clientUrl.password = password; + + log.debug(serviceLoadedMsg(`${serviceName}client`)); + return clientUrl.toString(); +} + +export function createEsClient(config: ScoutServerConfig, log: ToolingLog) { + const { username, password } = config.auth; + const elasticsearchUrl = createClientUrlWithAuth({ + serviceName: 'Es', + url: config.hosts.elasticsearch, + username, + password, + log, + }); + + return createEsClientForTesting({ + esUrl: elasticsearchUrl, + authOverride: { username, password }, + }); +} + +export function createKbnClient(config: ScoutServerConfig, log: ToolingLog) { + const kibanaUrl = createClientUrlWithAuth({ + serviceName: 'Kbn', + url: config.hosts.kibana, + username: config.auth.username, + password: config.auth.password, + log, + }); + + return new KbnClient({ log, url: kibanaUrl }); +} diff --git a/packages/kbn-scout/src/common/services/config.ts b/packages/kbn-scout/src/common/services/config.ts new file mode 100644 index 0000000000000..fe8e932194d91 --- /dev/null +++ b/packages/kbn-scout/src/common/services/config.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import fs from 'fs'; +import { ToolingLog } from '@kbn/tooling-log'; +import { ScoutServerConfig } from '../../types'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +export function createScoutConfig(configDir: string, configName: string, log: ToolingLog) { + if (!configDir || !fs.existsSync(configDir)) { + throw new Error(`Directory with servers configuration is missing`); + } + + const configPath = path.join(configDir, `${configName}.json`); + log.info(`Reading test servers confiuration from file: ${configPath}`); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as ScoutServerConfig; + + log.debug(serviceLoadedMsg('config')); + + return config; +} diff --git a/packages/kbn-scout/src/common/services/es_archiver.ts b/packages/kbn-scout/src/common/services/es_archiver.ts new file mode 100644 index 0000000000000..38b86d800459f --- /dev/null +++ b/packages/kbn-scout/src/common/services/es_archiver.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Client } from '@elastic/elasticsearch'; +import { EsArchiver } from '@kbn/es-archiver'; +import { REPO_ROOT } from '@kbn/repo-info'; +import type { KbnClient } from '@kbn/test'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +export function createEsArchiver(esClient: Client, kbnClient: KbnClient, log: ToolingLog) { + const esArchiver = new EsArchiver({ + log, + client: esClient, + kbnClient, + baseDir: REPO_ROOT, + }); + + log.debug(serviceLoadedMsg('esArchiver')); + + return esArchiver; +} diff --git a/packages/kbn-scout/src/common/services/index.ts b/packages/kbn-scout/src/common/services/index.ts new file mode 100644 index 0000000000000..6368e613c0284 --- /dev/null +++ b/packages/kbn-scout/src/common/services/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { createEsClient, createKbnClient } from './clients'; +export { createScoutConfig } from './config'; +export { createEsArchiver } from './es_archiver'; +export { createKbnUrl } from './kibana_url'; +export { createSamlSessionManager } from './saml_auth'; +export { createLogger } from './logger'; + +export type { KibanaUrl } from './kibana_url'; diff --git a/packages/kbn-scout/src/common/services/kibana_url.ts b/packages/kbn-scout/src/common/services/kibana_url.ts new file mode 100644 index 0000000000000..cbfab5dc90796 --- /dev/null +++ b/packages/kbn-scout/src/common/services/kibana_url.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import { ScoutServerConfig } from '../../types'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +export interface PathOptions { + /** + * Query string parameters + */ + params?: Record; + /** + * The hash value of the URL + */ + hash?: string; +} + +export class KibanaUrl { + #baseUrl: URL; + + constructor(baseUrl: URL) { + this.#baseUrl = baseUrl; + } + + /** + * Get an absolute URL based on Kibana's URL + * @param rel relative url, resolved relative to Kibana's url + * @param options optional modifications to apply to the URL + */ + get(rel?: string, options?: PathOptions) { + const url = new URL(rel ?? '/', this.#baseUrl); + + if (options?.params) { + for (const [key, value] of Object.entries(options.params)) { + url.searchParams.set(key, value); + } + } + + if (options?.hash !== undefined) { + url.hash = options.hash; + } + + return url.href; + } + + /** + * Get the URL for an app + * @param appName name of the app to get the URL for + * @param options optional modifications to apply to the URL + */ + app(appName: string, options?: PathOptions) { + return this.get(`/app/${appName}`, options); + } + + toString() { + return this.#baseUrl.href; + } +} + +export function createKbnUrl(scoutConfig: ScoutServerConfig, log: ToolingLog) { + const kbnUrl = new KibanaUrl(new URL(scoutConfig.hosts.kibana)); + + log.debug(serviceLoadedMsg('kbnUrl')); + + return kbnUrl; +} diff --git a/packages/kbn-scout/src/common/services/logger.ts b/packages/kbn-scout/src/common/services/logger.ts new file mode 100644 index 0000000000000..4ab39ba7dec68 --- /dev/null +++ b/packages/kbn-scout/src/common/services/logger.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +export function createLogger() { + const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout }); + + log.debug(serviceLoadedMsg('logger')); + + return log; +} diff --git a/packages/kbn-scout/src/common/services/saml_auth.ts b/packages/kbn-scout/src/common/services/saml_auth.ts new file mode 100644 index 0000000000000..e3dbd47fc8c90 --- /dev/null +++ b/packages/kbn-scout/src/common/services/saml_auth.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import { URL } from 'url'; +import { + SERVERLESS_ROLES_ROOT_PATH, + STATEFUL_ROLES_ROOT_PATH, + readRolesDescriptorsFromResource, +} from '@kbn/es'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { HostOptions, SamlSessionManager } from '@kbn/test'; +import { ToolingLog } from '@kbn/tooling-log'; +import { ScoutServerConfig } from '../../types'; +import { Protocol } from '../../playwright/types'; +import { serviceLoadedMsg } from '../../playwright/utils'; + +const getResourceDirPath = (config: ScoutServerConfig) => { + return config.serverless + ? path.resolve(SERVERLESS_ROLES_ROOT_PATH, config.projectType!) + : path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH); +}; + +const createKibanaHostOptions = (config: ScoutServerConfig): HostOptions => { + const kibanaUrl = new URL(config.hosts.kibana); + kibanaUrl.username = config.auth.username; + kibanaUrl.password = config.auth.password; + + return { + protocol: kibanaUrl.protocol.replace(':', '') as Protocol, + hostname: kibanaUrl.hostname, + port: Number(kibanaUrl.port), + username: kibanaUrl.username, + password: kibanaUrl.password, + }; +}; + +export const createSamlSessionManager = ( + config: ScoutServerConfig, + log: ToolingLog +): SamlSessionManager => { + const resourceDirPath = getResourceDirPath(config); + const rolesDefinitionPath = path.resolve(resourceDirPath, 'roles.yml'); + + const supportedRoleDescriptors = readRolesDescriptorsFromResource(rolesDefinitionPath) as Record< + string, + unknown + >; + const supportedRoles = Object.keys(supportedRoleDescriptors); + + const sessionManager = new SamlSessionManager({ + hostOptions: createKibanaHostOptions(config), + log, + isCloud: config.isCloud, + supportedRoles: { + roles: supportedRoles, + sourcePath: rolesDefinitionPath, + }, + cloudUsersFilePath: config.cloudUsersFilePath, + }); + + log.debug(serviceLoadedMsg('samlAuth')); + + return sessionManager; +}; diff --git a/packages/kbn-scout/src/common/utils/index.ts b/packages/kbn-scout/src/common/utils/index.ts new file mode 100644 index 0000000000000..0ab702b0cdfde --- /dev/null +++ b/packages/kbn-scout/src/common/utils/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import * as Rx from 'rxjs'; + +export async function silence(log: ToolingLog, milliseconds: number) { + await Rx.firstValueFrom( + log.getWritten$().pipe( + Rx.startWith(null), + Rx.switchMap(() => Rx.timer(milliseconds)) + ) + ); +} diff --git a/packages/kbn-scout/src/config/config.ts b/packages/kbn-scout/src/config/config.ts new file mode 100644 index 0000000000000..a316aac61d69e --- /dev/null +++ b/packages/kbn-scout/src/config/config.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Schema } from 'joi'; +import * as Url from 'url'; +import Path from 'path'; +import { cloneDeepWith, get, has, toPath } from 'lodash'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { schema } from './schema'; +import { ScoutServerConfig } from '../types'; +import { formatCurrentDate, getProjectType } from './utils'; + +const $values = Symbol('values'); + +export class Config { + private [$values]: Record; + + constructor(data: Record) { + const { error, value } = schema.validate(data, { + abortEarly: false, + }); + + if (error) { + throw error; + } + + this[$values] = value; + } + + public has(key: string | string[]) { + function recursiveHasCheck( + remainingPath: string[], + values: Record, + childSchema: any + ): boolean { + if (!childSchema.$_terms.keys && !childSchema.$_terms.patterns) { + return false; + } + + // normalize child and pattern checks so we can iterate the checks in a single loop + const checks: Array<{ test: (k: string) => boolean; schema: Schema }> = [ + // match children first, they have priority + ...(childSchema.$_terms.keys || []).map((child: { key: string; schema: Schema }) => ({ + test: (k: string) => child.key === k, + schema: child.schema, + })), + + // match patterns on any key that doesn't match an explicit child + ...(childSchema.$_terms.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({ + test: (k: string) => pattern.regex.test(k) && has(values, k), + schema: pattern.rule, + })), + ]; + + for (const check of checks) { + if (!check.test(remainingPath[0])) { + continue; + } + + if (remainingPath.length > 1) { + return recursiveHasCheck( + remainingPath.slice(1), + get(values, remainingPath[0]), + check.schema + ); + } + + return true; + } + + return false; + } + + const path = toPath(key); + if (!path.length) { + return true; + } + return recursiveHasCheck(path, this[$values], schema); + } + + public get(key: string | string[], defaultValue?: any) { + if (!this.has(key)) { + throw new Error(`Unknown config key "${key}"`); + } + + return cloneDeepWith(get(this[$values], key, defaultValue), (v) => { + if (typeof v === 'function') { + return v; + } + }); + } + + public getAll() { + return cloneDeepWith(this[$values], (v) => { + if (typeof v === 'function') { + return v; + } + }); + } + + public getTestServersConfig(): ScoutServerConfig { + return { + serverless: this.get('serverless'), + projectType: this.get('serverless') + ? getProjectType(this.get('kbnTestServer.serverArgs')) + : undefined, + isCloud: false, + cloudUsersFilePath: Path.resolve(REPO_ROOT, '.ftr', 'role_users.json'), + hosts: { + kibana: Url.format({ + protocol: this.get('servers.kibana.protocol'), + hostname: this.get('servers.kibana.hostname'), + port: this.get('servers.kibana.port'), + }), + elasticsearch: Url.format({ + protocol: this.get('servers.elasticsearch.protocol'), + hostname: this.get('servers.elasticsearch.hostname'), + port: this.get('servers.elasticsearch.port'), + }), + }, + auth: { + username: this.get('servers.kibana.username'), + password: this.get('servers.kibana.password'), + }, + + metadata: { + generatedOn: formatCurrentDate(), + config: this.getAll(), + }, + }; + } +} diff --git a/packages/kbn-scout/src/config/constants.ts b/packages/kbn-scout/src/config/constants.ts new file mode 100644 index 0000000000000..c1593f23b35ee --- /dev/null +++ b/packages/kbn-scout/src/config/constants.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { resolve } from 'path'; +import { REPO_ROOT } from '@kbn/repo-info'; + +const SECURITY_TEST_PATH = resolve(REPO_ROOT, 'x-pack/test/security_api_integration'); + +export const SAML_IDP_PLUGIN_PATH = resolve(SECURITY_TEST_PATH, 'plugins/saml_provider'); + +export const STATEFUL_IDP_METADATA_PATH = resolve( + SECURITY_TEST_PATH, + 'packages/helpers/saml/idp_metadata_mock_idp.xml' +); +export const SERVERLESS_IDP_METADATA_PATH = resolve(SAML_IDP_PLUGIN_PATH, 'metadata.xml'); +export const JWKS_PATH = resolve(SECURITY_TEST_PATH, 'packages/helpers/oidc/jwks.json'); diff --git a/packages/kbn-scout/src/config/get_config_file.ts b/packages/kbn-scout/src/config/get_config_file.ts new file mode 100644 index 0000000000000..5976db1265797 --- /dev/null +++ b/packages/kbn-scout/src/config/get_config_file.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import { CliSupportedServerModes } from '../types'; + +export const getConfigFilePath = (config: CliSupportedServerModes): string => { + if (config === 'stateful') { + return path.join(__dirname, 'stateful', 'stateful.config.ts'); + } + + const [mode, type] = config.split('='); + if (mode !== 'serverless' || !type) { + throw new Error( + `Invalid config format: ${config}. Expected "stateful" or "serverless=".` + ); + } + + return path.join(__dirname, 'serverless', `${type}.serverless.config.ts`); +}; diff --git a/packages/kbn-scout/src/config/index.ts b/packages/kbn-scout/src/config/index.ts new file mode 100644 index 0000000000000..969edbe8e4483 --- /dev/null +++ b/packages/kbn-scout/src/config/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { loadConfig } from './loader/config_load'; +export { getConfigFilePath } from './get_config_file'; +export { loadServersConfig } from './utils'; +export type { Config } from './config'; diff --git a/packages/kbn-scout/src/config/loader/config_load.ts b/packages/kbn-scout/src/config/loader/config_load.ts new file mode 100644 index 0000000000000..5ef4b88b4cf1a --- /dev/null +++ b/packages/kbn-scout/src/config/loader/config_load.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { Config } from '../config'; + +export const loadConfig = async (configPath: string, log: ToolingLog): Promise => { + try { + const absolutePath = path.resolve(configPath); + const configModule = await import(absolutePath); + + if (configModule.servers) { + return new Config(configModule.servers); + } else { + throw new Error(`No 'servers' found in the config file at path: ${absolutePath}`); + } + } catch (error) { + throw new Error(`Failed to load config from ${configPath}: ${error.message}`); + } +}; diff --git a/packages/kbn-scout/src/config/schema/index.ts b/packages/kbn-scout/src/config/schema/index.ts new file mode 100644 index 0000000000000..7fa3cbc65f29f --- /dev/null +++ b/packages/kbn-scout/src/config/schema/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { schema } from './schema'; diff --git a/packages/kbn-scout/src/config/schema/schema.ts b/packages/kbn-scout/src/config/schema/schema.ts new file mode 100644 index 0000000000000..86add154cc661 --- /dev/null +++ b/packages/kbn-scout/src/config/schema/schema.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Joi from 'joi'; + +const maybeRequireKeys = (keys: string[], schemas: Record) => { + if (!keys.length) { + return schemas; + } + + const withRequires: Record = {}; + for (const [key, schema] of Object.entries(schemas)) { + withRequires[key] = keys.includes(key) ? schema.required() : schema; + } + return withRequires; +}; + +const urlPartsSchema = ({ requiredKeys }: { requiredKeys?: string[] } = {}) => + Joi.object() + .keys( + maybeRequireKeys(requiredKeys ?? [], { + protocol: Joi.string().valid('http', 'https').default('http'), + hostname: Joi.string().hostname().default('localhost'), + port: Joi.number(), + auth: Joi.string().regex(/^[^:]+:.+$/, 'username and password separated by a colon'), + username: Joi.string(), + password: Joi.string(), + pathname: Joi.string().regex(/^\//, 'start with a /'), + hash: Joi.string().regex(/^\//, 'start with a /'), + certificateAuthorities: Joi.array().items(Joi.binary()).optional(), + }) + ) + .default(); + +const requiredWhenEnabled = (schema: Joi.Schema) => { + return Joi.when('enabled', { + is: true, + then: schema.required(), + otherwise: schema.optional(), + }); +}; + +const dockerServerSchema = () => + Joi.object() + .keys({ + enabled: Joi.boolean().required(), + image: requiredWhenEnabled(Joi.string()), + port: requiredWhenEnabled(Joi.number()), + portInContainer: requiredWhenEnabled(Joi.number()), + waitForLogLine: Joi.alternatives(Joi.object().instance(RegExp), Joi.string()).optional(), + waitForLogLineTimeoutMs: Joi.number().integer().optional(), + waitFor: Joi.func().optional(), + args: Joi.array().items(Joi.string()).optional(), + }) + .default(); + +export const schema = Joi.object() + .keys({ + serverless: Joi.boolean().default(false), + servers: Joi.object() + .keys({ + kibana: urlPartsSchema(), + elasticsearch: urlPartsSchema({ + requiredKeys: ['port'], + }), + fleetserver: urlPartsSchema(), + }) + .default(), + + esTestCluster: Joi.object() + .keys({ + license: Joi.valid('basic', 'trial', 'gold').default('basic'), + from: Joi.string().default('snapshot'), + serverArgs: Joi.array().items(Joi.string()).default([]), + esJavaOpts: Joi.string(), + dataArchive: Joi.string(), + ssl: Joi.boolean().default(false), + ccs: Joi.object().keys({ + remoteClusterUrl: Joi.string().uri({ + scheme: /https?/, + }), + }), + files: Joi.array().items(Joi.string()), + }) + .default(), + + esServerlessOptions: Joi.object() + .keys({ + host: Joi.string().ip(), + resources: Joi.array().items(Joi.string()).default([]), + }) + .default(), + + kbnTestServer: Joi.object() + .keys({ + buildArgs: Joi.array(), + sourceArgs: Joi.array(), + serverArgs: Joi.array(), + installDir: Joi.string(), + useDedicatedTaskRunner: Joi.boolean().default(false), + /** Options for how FTR should execute and interact with Kibana */ + runOptions: Joi.object() + .keys({ + /** + * Log message to wait for before initiating tests, defaults to waiting for Kibana status to be `available`. + * Note that this log message must not be filtered out by the current logging config, for example by the + * log level. If needed, you can adjust the logging level via `kbnTestServer.serverArgs`. + */ + wait: Joi.object() + .regex() + .default(/Kibana is now available/), + + /** + * Does this test config only work when run against source? + */ + alwaysUseSource: Joi.boolean().default(false), + }) + .default(), + env: Joi.object().unknown().default(), + delayShutdown: Joi.number(), + }) + .default(), + + // settings for the kibanaServer.uiSettings module + uiSettings: Joi.object() + .keys({ + defaults: Joi.object().unknown(true), + }) + .default(), + + dockerServers: Joi.object().pattern(Joi.string(), dockerServerSchema()).default(), + }) + .default(); diff --git a/packages/kbn-scout/src/config/serverless/es.serverless.config.ts b/packages/kbn-scout/src/config/serverless/es.serverless.config.ts new file mode 100644 index 0000000000000..89e27b4e877e0 --- /dev/null +++ b/packages/kbn-scout/src/config/serverless/es.serverless.config.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutLoaderConfig } from '../../types'; +import { defaultConfig } from './serverless.base.config'; + +export const servers: ScoutLoaderConfig = { + ...defaultConfig, + esTestCluster: { + ...defaultConfig.esTestCluster, + serverArgs: [...defaultConfig.esTestCluster.serverArgs], + }, + kbnTestServer: { + serverArgs: [ + ...defaultConfig.kbnTestServer.serverArgs, + '--serverless=es', + '--coreApp.allowDynamicConfigOverrides=true', + ], + }, +}; diff --git a/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts b/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts new file mode 100644 index 0000000000000..3f283f140479e --- /dev/null +++ b/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { defaultConfig } from './serverless.base.config'; +import { ScoutLoaderConfig } from '../../types'; + +export const servers: ScoutLoaderConfig = { + ...defaultConfig, + esTestCluster: { + ...defaultConfig.esTestCluster, + serverArgs: [ + ...defaultConfig.esTestCluster.serverArgs, + 'xpack.apm_data.enabled=true', + // for ML, data frame analytics are not part of this project type + 'xpack.ml.dfa.enabled=false', + ], + }, + kbnTestServer: { + serverArgs: [ + ...defaultConfig.kbnTestServer.serverArgs, + '--serverless=oblt', + '--coreApp.allowDynamicConfigOverrides=true', + '--xpack.uptime.service.manifestUrl=mockDevUrl', + ], + }, +}; diff --git a/packages/kbn-scout/src/config/serverless/resources/package_registry_config.yml b/packages/kbn-scout/src/config/serverless/resources/package_registry_config.yml new file mode 100644 index 0000000000000..1885fa5c2ebe5 --- /dev/null +++ b/packages/kbn-scout/src/config/serverless/resources/package_registry_config.yml @@ -0,0 +1,2 @@ +package_paths: + - /packages/package-storage diff --git a/packages/kbn-scout/src/config/serverless/security.serverless.config.ts b/packages/kbn-scout/src/config/serverless/security.serverless.config.ts new file mode 100644 index 0000000000000..f1fa4f53f8988 --- /dev/null +++ b/packages/kbn-scout/src/config/serverless/security.serverless.config.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutLoaderConfig } from '../../types'; +import { defaultConfig } from './serverless.base.config'; + +export const servers: ScoutLoaderConfig = { + ...defaultConfig, + esTestCluster: { + ...defaultConfig.esTestCluster, + serverArgs: [ + ...defaultConfig.esTestCluster.serverArgs, + 'xpack.security.authc.api_key.cache.max_keys=70000', + ], + }, + kbnTestServer: { + serverArgs: [ + ...defaultConfig.kbnTestServer.serverArgs, + '--serverless=security', + '--coreApp.allowDynamicConfigOverrides=true', + `--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['Fleet-Metrics-Task'])}`, + ], + }, +}; diff --git a/packages/kbn-scout/src/config/serverless/serverless.base.config.ts b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts new file mode 100644 index 0000000000000..8b4852f9c9e62 --- /dev/null +++ b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { resolve, join } from 'path'; +import { format as formatUrl } from 'url'; +import Fs from 'fs'; + +import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils'; +import { defineDockerServersConfig, getDockerFileMountPath } from '@kbn/test'; +import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; + +import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { ScoutLoaderConfig } from '../../types'; +import { SAML_IDP_PLUGIN_PATH, SERVERLESS_IDP_METADATA_PATH, JWKS_PATH } from '../constants'; + +const packageRegistryConfig = join(__dirname, './package_registry_config.yml'); +const dockerArgs: string[] = ['-v', `${packageRegistryConfig}:/package-registry/config.yml`]; + +/** + * This is used by CI to set the docker registry port + * you can also define this environment variable locally when running tests which + * will spin up a local docker package registry locally for you + * if this is defined it takes precedence over the `packageRegistryOverride` variable + */ +const dockerRegistryPort: string | undefined = process.env.FLEET_PACKAGE_REGISTRY_PORT; + +const servers = { + elasticsearch: { + protocol: 'https', + hostname: 'localhost', + port: 9220, + username: 'elastic_serverless', + password: 'changeme', + certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)], + }, + kibana: { + protocol: 'http', + hostname: 'localhost', + port: 5620, + username: 'elastic_serverless', + password: 'changeme', + }, +}; + +export const defaultConfig: ScoutLoaderConfig = { + serverless: true, + servers, + dockerServers: defineDockerServersConfig({ + registry: { + enabled: !!dockerRegistryPort, + image: dockerImage, + portInContainer: 8080, + port: dockerRegistryPort, + args: dockerArgs, + waitForLogLine: 'package manifests loaded', + waitForLogLineTimeoutMs: 60 * 2 * 1000, // 2 minutes + }, + }), + esTestCluster: { + from: 'serverless', + files: [SERVERLESS_IDP_METADATA_PATH, JWKS_PATH], + serverArgs: [ + 'xpack.security.authc.realms.file.file1.order=-100', + `xpack.security.authc.realms.native.native1.enabled=false`, + `xpack.security.authc.realms.native.native1.order=-97`, + + 'xpack.security.authc.realms.jwt.jwt1.allowed_audiences=elasticsearch', + `xpack.security.authc.realms.jwt.jwt1.allowed_issuer=https://kibana.elastic.co/jwt/`, + `xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms=[RS256]`, + `xpack.security.authc.realms.jwt.jwt1.allowed_subjects=elastic-agent`, + `xpack.security.authc.realms.jwt.jwt1.claims.principal=sub`, + 'xpack.security.authc.realms.jwt.jwt1.client_authentication.type=shared_secret', + 'xpack.security.authc.realms.jwt.jwt1.order=-98', + `xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${getDockerFileMountPath(JWKS_PATH)}`, + `xpack.security.authc.realms.jwt.jwt1.token_type=access_token`, + ], + ssl: true, // SSL is required for SAML realm + }, + kbnTestServer: { + buildArgs: [], + env: { + KBN_PATH_CONF: resolve(REPO_ROOT, 'config'), + }, + sourceArgs: ['--no-base-path', '--env.name=development'], + serverArgs: [ + `--server.restrictInternalApis=true`, + `--server.port=${servers.kibana.port}`, + '--status.allowAnonymous=true', + `--migrations.zdt.runOnRoles=${JSON.stringify(['ui'])}`, + // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should + // either include `kibanaServerTestUser` credentials, or credentials provided by the test + // user, or none at all in case anonymous access is used. + `--elasticsearch.hosts=${formatUrl( + Object.fromEntries( + Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth') + ) + )}`, + `--elasticsearch.serviceAccountToken=${kibanaDevServiceAccount.token}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + '--telemetry.sendUsageTo=staging', + `--logging.appenders.deprecation=${JSON.stringify({ + type: 'console', + layout: { + type: 'json', + }, + })}`, + `--logging.loggers=${JSON.stringify([ + { + name: 'elasticsearch.deprecation', + level: 'all', + appenders: ['deprecation'], + }, + ])}`, + // Add meta info to the logs so FTR logs are more actionable + `--logging.appenders.default=${JSON.stringify({ + type: 'console', + layout: { + type: 'pattern', + pattern: '[%date][%level][%logger] %message %meta', + }, + })}`, + `--logging.appenders.console=${JSON.stringify({ + type: 'console', + layout: { + type: 'pattern', + pattern: '[%date][%level][%logger] %message %meta', + }, + })}`, + // This ensures that we register the Security SAML API endpoints. + // In the real world the SAML config is injected by control plane. + `--plugin-path=${SAML_IDP_PLUGIN_PATH}`, + '--xpack.cloud.id=ftr_fake_cloud_id', + // Ensure that SAML is used as the default authentication method whenever a user navigates to Kibana. In other + // words, Kibana should attempt to authenticate the user using the provider with the lowest order if the Login + // Selector is disabled (which is how Serverless Kibana is configured). By declaring `cloud-basic` with a higher + // order, we indicate that basic authentication can still be used, but only if explicitly requested when the + // user navigates to `/login` page directly and enters username and password in the login form. + '--xpack.security.authc.selector.enabled=false', + `--xpack.security.authc.providers=${JSON.stringify({ + saml: { 'cloud-saml-kibana': { order: 0, realm: MOCK_IDP_REALM_NAME } }, + basic: { 'cloud-basic': { order: 1 } }, + })}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + `--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`, + // configure security reponse header report-to settings to mimic MKI configuration + `--csp.report_to=${JSON.stringify(['violations-endpoint'])}`, + `--permissionsPolicy.report_to=${JSON.stringify(['violations-endpoint'])}`, + ], + }, +}; diff --git a/packages/kbn-scout/src/config/stateful/base.config.ts b/packages/kbn-scout/src/config/stateful/base.config.ts new file mode 100644 index 0000000000000..a2d6f1e0fa6eb --- /dev/null +++ b/packages/kbn-scout/src/config/stateful/base.config.ts @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { join } from 'path'; +import { format as formatUrl } from 'url'; + +import { + MOCK_IDP_ENTITY_ID, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ATTRIBUTE_EMAIL, + MOCK_IDP_ATTRIBUTE_NAME, +} from '@kbn/mock-idp-utils'; +import { defineDockerServersConfig } from '@kbn/test'; +import path from 'path'; + +import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; + +import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es'; +import type { ScoutLoaderConfig } from '../../types'; +import { SAML_IDP_PLUGIN_PATH, STATEFUL_IDP_METADATA_PATH } from '../constants'; + +const packageRegistryConfig = join(__dirname, './package_registry_config.yml'); +const dockerArgs: string[] = ['-v', `${packageRegistryConfig}:/package-registry/config.yml`]; + +/** + * This is used by CI to set the docker registry port + * you can also define this environment variable locally when running tests which + * will spin up a local docker package registry locally for you + * if this is defined it takes precedence over the `packageRegistryOverride` variable + */ +const dockerRegistryPort: string | undefined = process.env.FLEET_PACKAGE_REGISTRY_PORT; + +// if config is executed on CI or locally +const isRunOnCI = process.env.CI; + +const servers = { + elasticsearch: { + protocol: 'http', + hostname: 'localhost', + port: 9220, + username: 'kibana_system', + password: 'changeme', + }, + kibana: { + protocol: 'http', + hostname: 'localhost', + port: 5620, + username: 'elastic', + password: 'changeme', + }, +}; + +const kbnUrl = `${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`; + +export const defaultConfig: ScoutLoaderConfig = { + servers, + dockerServers: defineDockerServersConfig({ + registry: { + enabled: !!dockerRegistryPort, + image: dockerImage, + portInContainer: 8080, + port: dockerRegistryPort, + args: dockerArgs, + waitForLogLine: 'package manifests loaded', + waitForLogLineTimeoutMs: 60 * 2 * 1000, // 2 minutes + }, + }), + esTestCluster: { + from: 'snapshot', + license: 'trial', + files: [ + // Passing the roles that are equivalent to the ones we have in serverless + path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml'), + ], + serverArgs: [ + 'path.repo=/tmp/', + 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,/tmp/cloud-snapshots/', + 'node.attr.name=apiIntegrationTestNode', + 'xpack.security.authc.api_key.enabled=true', + 'xpack.security.authc.token.enabled=true', + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order=0`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path=${STATEFUL_IDP_METADATA_PATH}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id=${MOCK_IDP_ENTITY_ID}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id=${kbnUrl}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs=${kbnUrl}/api/security/saml/callback`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout=${kbnUrl}/logout`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal=${MOCK_IDP_ATTRIBUTE_PRINCIPAL}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups=${MOCK_IDP_ATTRIBUTE_ROLES}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name=${MOCK_IDP_ATTRIBUTE_NAME}`, + `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail=${MOCK_IDP_ATTRIBUTE_EMAIL}`, + ], + ssl: false, + }, + kbnTestServer: { + buildArgs: [], + env: {}, + sourceArgs: ['--no-base-path', '--env.name=development'], + serverArgs: [ + `--server.port=${servers.kibana.port}`, + '--status.allowAnonymous=true', + // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should + // either include `kibanaServerTestUser` credentials, or credentials provided by the test + // user, or none at all in case anonymous access is used. + `--elasticsearch.hosts=${formatUrl( + Object.fromEntries( + Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth') + ) + )}`, + `--elasticsearch.username=${servers.elasticsearch.username}`, + `--elasticsearch.password=${servers.elasticsearch.password}`, + // Needed for async search functional tests to introduce a delay + `--data.search.aggs.shardDelay.enabled=true`, + `--data.query.timefilter.minRefreshInterval=1000`, + `--security.showInsecureClusterWarning=false`, + '--telemetry.banner=false', + '--telemetry.optIn=false', + // These are *very* important to have them pointing to staging + '--telemetry.sendUsageTo=staging', + `--server.maxPayload=1679958`, + // newsfeed mock service + `--plugin-path=${path.join(REPO_ROOT, 'test', 'common', 'plugins', 'newsfeed')}`, + // otel mock service + `--plugin-path=${path.join(REPO_ROOT, 'test', 'common', 'plugins', 'otel_metrics')}`, + `--newsfeed.service.urlRoot=${kbnUrl}`, + `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`, + `--logging.appenders.deprecation=${JSON.stringify({ + type: 'console', + layout: { + type: 'json', + }, + })}`, + `--logging.loggers=${JSON.stringify([ + { + name: 'elasticsearch.deprecation', + level: 'all', + appenders: ['deprecation'], + }, + ])}`, + // Add meta info to the logs so FTR logs are more actionable + `--logging.appenders.default=${JSON.stringify({ + type: 'console', + layout: { + type: 'pattern', + pattern: '[%date][%level][%logger] %message %meta', + }, + })}`, + `--logging.appenders.console=${JSON.stringify({ + type: 'console', + layout: { + type: 'pattern', + pattern: '[%date][%level][%logger] %message %meta', + }, + })}`, + // x-pack/test/functional/config.base.js + '--status.allowAnonymous=true', + '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', + '--xpack.maps.showMapsInspectorAdapter=true', + '--xpack.maps.preserveDrawingBuffer=true', + '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions + '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', + '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', + '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects, + '--savedObjects.allowHttpApiAccess=false', // override default to not allow hiddenFromHttpApis saved objects access to the http APIs see https://github.com/elastic/dev/issues/2200 + // explicitly disable internal API restriction. See https://github.com/elastic/kibana/issues/163654 + '--server.restrictInternalApis=false', + // disable fleet task that writes to metrics.fleet_server.* data streams, impacting functional tests + `--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['Fleet-Metrics-Task'])}`, + // x-pack/test/api_integration/config.ts + '--xpack.security.session.idleTimeout=3600000', // 1 hour + '--telemetry.optIn=true', + '--xpack.fleet.agents.pollingRequestTimeout=5000', // 5 seconds + '--xpack.ruleRegistry.write.enabled=true', + '--xpack.ruleRegistry.write.enabled=true', + '--xpack.ruleRegistry.write.cache.enabled=false', + '--monitoring_collection.opentelemetry.metrics.prometheus.enabled=true', + // SAML configuration + ...(isRunOnCI ? [] : ['--mock_idp_plugin.enabled=true']), + // This ensures that we register the Security SAML API endpoints. + // In the real world the SAML config is injected by control plane. + `--plugin-path=${SAML_IDP_PLUGIN_PATH}`, + '--xpack.cloud.id=ftr_fake_cloud_id', + // Ensure that SAML is used as the default authentication method whenever a user navigates to Kibana. In other + // words, Kibana should attempt to authenticate the user using the provider with the lowest order if the Login + // Selector is disabled (replicating Serverless configuration). By declaring `cloud-basic` with a higher + // order, we indicate that basic authentication can still be used, but only if explicitly requested when the + // user navigates to `/login` page directly and enters username and password in the login form. + '--xpack.security.authc.selector.enabled=false', + `--xpack.security.authc.providers=${JSON.stringify({ + saml: { 'cloud-saml-kibana': { order: 0, realm: MOCK_IDP_REALM_NAME } }, + basic: { 'cloud-basic': { order: 1 } }, + })}`, + `--server.publicBaseUrl=${kbnUrl}`, + ], + }, +}; diff --git a/packages/kbn-scout/src/config/stateful/stateful.config.ts b/packages/kbn-scout/src/config/stateful/stateful.config.ts new file mode 100644 index 0000000000000..e67419c21fb37 --- /dev/null +++ b/packages/kbn-scout/src/config/stateful/stateful.config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutLoaderConfig } from '../../types'; +import { defaultConfig } from './base.config'; + +export const servers: ScoutLoaderConfig = defaultConfig; diff --git a/packages/kbn-scout/src/config/utils.ts b/packages/kbn-scout/src/config/utils.ts new file mode 100644 index 0000000000000..61bdc1b7b81ac --- /dev/null +++ b/packages/kbn-scout/src/config/utils.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import * as Fs from 'fs'; +import getopts from 'getopts'; +import path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { ServerlessProjectType } from '@kbn/es'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { CliSupportedServerModes, ScoutServerConfig } from '../types'; +import { getConfigFilePath } from './get_config_file'; +import { loadConfig } from './loader/config_load'; + +export const formatCurrentDate = () => { + const now = new Date(); + + const format = (num: number, length: number) => String(num).padStart(length, '0'); + + return ( + `${format(now.getDate(), 2)}/${format(now.getMonth() + 1, 2)}/${now.getFullYear()} ` + + `${format(now.getHours(), 2)}:${format(now.getMinutes(), 2)}:${format(now.getSeconds(), 2)}.` + + `${format(now.getMilliseconds(), 3)}` + ); +}; + +const saveTestServersConfigOnDisk = (testServersConfig: ScoutServerConfig, log: ToolingLog) => { + const configDirPath = path.resolve(REPO_ROOT, '.scout', 'servers'); + const configFilePath = path.join(configDirPath, `local.json`); + + try { + const jsonData = JSON.stringify(testServersConfig, null, 2); + + if (!Fs.existsSync(configDirPath)) { + log.debug(`scout: creating configuration directory: ${configDirPath}`); + Fs.mkdirSync(configDirPath, { recursive: true }); + } + + Fs.writeFileSync(configFilePath, jsonData, 'utf-8'); + log.info(`scout: Test server configuration saved at ${configFilePath}`); + } catch (error) { + log.error(`scout: Failed to save test server configuration - ${error.message}`); + throw new Error(`Failed to save test server configuration at ${configFilePath}`); + } +}; + +export async function loadServersConfig(mode: CliSupportedServerModes, log: ToolingLog) { + // get path to one of the predefined config files + const configPath = getConfigFilePath(mode); + // load config that is compatible with kbn-test input format + const config = await loadConfig(configPath, log); + // construct config for Playwright Test + const scoutServerConfig = config.getTestServersConfig(); + // save test config to the file + saveTestServersConfigOnDisk(scoutServerConfig, log); + + return config; +} + +export const getProjectType = (kbnServerArgs: string[]) => { + const options = getopts(kbnServerArgs); + return options.serverless as ServerlessProjectType; +}; diff --git a/packages/kbn-scout/src/playwright/config/index.ts b/packages/kbn-scout/src/playwright/config/index.ts new file mode 100644 index 0000000000000..62f5261c08e25 --- /dev/null +++ b/packages/kbn-scout/src/playwright/config/index.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { defineConfig, PlaywrightTestConfig, devices } from '@playwright/test'; +import * as Path from 'path'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { ScoutPlaywrightOptions, ScoutTestOptions, VALID_CONFIG_MARKER } from '../types'; + +export function createPlaywrightConfig(options: ScoutPlaywrightOptions): PlaywrightTestConfig { + return defineConfig({ + testDir: options.testDir, + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: options.workers ?? 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html', { outputFolder: './output/reports', open: 'never' }], // HTML report configuration + ['json', { outputFile: './output/reports/test-results.json' }], // JSON report + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + serversConfigDir: Path.resolve(REPO_ROOT, '.scout', 'servers'), + [VALID_CONFIG_MARKER]: true, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + // video: 'retain-on-failure', + // storageState: './output/reports/state.json', // Store session state (like cookies) + }, + + // Timeout for each test, includes test, hooks and fixtures + timeout: 60000, + + // Timeout for each assertion + expect: { + timeout: 10000, + }, + + outputDir: './output/test-artifacts', // For other test artifacts (screenshots, videos, traces) + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, + }); +} diff --git a/packages/kbn-scout/src/playwright/expect.ts b/packages/kbn-scout/src/playwright/expect.ts new file mode 100644 index 0000000000000..a75e30adf2631 --- /dev/null +++ b/packages/kbn-scout/src/playwright/expect.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test } from '@playwright/test'; + +// Export `expect` to avoid importing from Playwright directly +export const expect = test.expect; diff --git a/packages/kbn-scout/src/playwright/fixtures/index.ts b/packages/kbn-scout/src/playwright/fixtures/index.ts new file mode 100644 index 0000000000000..348b581005994 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { mergeTests } from '@playwright/test'; + +import { scoutWorkerFixtures } from './worker'; +import { scoutTestFixtures } from './test'; + +export const scoutCoreFixtures = mergeTests(scoutWorkerFixtures, scoutTestFixtures); + +export type { + ScoutTestFixtures, + ScoutWorkerFixtures, + ScoutPage, + Client, + KbnClient, + KibanaUrl, + ToolingLog, +} from './types'; diff --git a/packages/kbn-scout/src/playwright/fixtures/test/browser_auth.ts b/packages/kbn-scout/src/playwright/fixtures/test/browser_auth.ts new file mode 100644 index 0000000000000..5faa1b5392d96 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/test/browser_auth.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test as base } from '@playwright/test'; +import { PROJECT_DEFAULT_ROLES } from '../../../common'; +import { LoginFixture, ScoutWorkerFixtures } from '../types'; +import { serviceLoadedMsg } from '../../utils'; + +type LoginFunction = (role: string) => Promise; + +export const browserAuthFixture = base.extend<{ browserAuth: LoginFixture }, ScoutWorkerFixtures>({ + browserAuth: async ({ log, context, samlAuth, config }, use) => { + const setSessionCookie = async (cookieValue: string) => { + await context.clearCookies(); + await context.addCookies([ + { + name: 'sid', + value: cookieValue, + path: '/', + domain: 'localhost', + }, + ]); + }; + + const loginAs: LoginFunction = async (role) => { + const cookie = await samlAuth.getInteractiveUserSessionCookieWithRoleScope(role); + await setSessionCookie(cookie); + }; + + const loginAsAdmin = () => loginAs('admin'); + const loginAsViewer = () => loginAs('viewer'); + const loginAsPrivilegedUser = () => { + const roleName = config.serverless + ? PROJECT_DEFAULT_ROLES.get(config.projectType!)! + : 'editor'; + return loginAs(roleName); + }; + + log.debug(serviceLoadedMsg('browserAuth')); + await use({ loginAsAdmin, loginAsViewer, loginAsPrivilegedUser }); + }, +}); diff --git a/packages/kbn-scout/src/playwright/fixtures/test/index.ts b/packages/kbn-scout/src/playwright/fixtures/test/index.ts new file mode 100644 index 0000000000000..41bfedcf39dc7 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/test/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { mergeTests } from '@playwright/test'; +import { browserAuthFixture } from './browser_auth'; +import { scoutPageFixture } from './page'; +import { pageObjectsFixture } from './page_objects'; + +export const scoutTestFixtures = mergeTests( + browserAuthFixture, + scoutPageFixture, + pageObjectsFixture +); diff --git a/packages/kbn-scout/src/playwright/fixtures/test/page.ts b/packages/kbn-scout/src/playwright/fixtures/test/page.ts new file mode 100644 index 0000000000000..ffc309d37cbad --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/test/page.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Page, test as base } from '@playwright/test'; +import { subj } from '@kbn/test-subj-selector'; +import { ScoutPage, KibanaUrl } from '../types'; + +/** + * Instead of defining each method individually, we use a list of method names and loop through them, creating methods dynamically. + * All methods must have 'selector: string' as the first argument + */ +function extendPageWithTestSubject(page: Page) { + const methods: Array = [ + 'check', + 'click', + 'dblclick', + 'fill', + 'focus', + 'getAttribute', + 'hover', + 'isEnabled', + 'innerText', + 'isChecked', + 'isHidden', + 'locator', + ]; + + const extendedMethods: Partial> = {}; + + for (const method of methods) { + extendedMethods[method] = (...args: any[]) => { + const selector = args[0]; + const testSubjSelector = subj(selector); + return (page[method] as Function)(testSubjSelector, ...args.slice(1)); + }; + } + + return extendedMethods as Record; +} + +/** + * Extends the 'page' fixture with Kibana-specific functionality + * + * 1. Allow calling methods with simplified 'data-test-subj' selectors. + * Instead of manually constructing 'data-test-subj' selectors, this extension provides a `testSubj` object on the page + * Supported methods include `click`, `check`, `fill`, and others that interact with `data-test-subj`. + * + * Example Usage: + * + * ```typescript + * // Without `testSubj` extension: + * await page.locator('[data-test-subj="foo"][data-test-subj="bar"]').click(); + * + * // With `testSubj` extension: + * await page.testSubj.click('foo & bar'); + * ``` + * + * 2. Navigate to Kibana apps by using 'kbnUrl' fixture + * + * Example Usage: + * + * ```typescript + * // Navigate to '/app/discover' + * await page.gotoApp('discover); + * ``` + */ +export const scoutPageFixture = base.extend<{ page: ScoutPage; kbnUrl: KibanaUrl }>({ + page: async ({ page, kbnUrl }, use) => { + // Extend page with '@kbn/test-subj-selector' support + page.testSubj = extendPageWithTestSubject(page); + + // Method to navigate to specific Kibana apps + page.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName)); + + await use(page); + }, +}); diff --git a/packages/kbn-scout/src/playwright/fixtures/test/page_objects.ts b/packages/kbn-scout/src/playwright/fixtures/test/page_objects.ts new file mode 100644 index 0000000000000..ed142b48b3f9a --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/test/page_objects.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test as base } from '@playwright/test'; +import { ScoutTestFixtures, ScoutWorkerFixtures } from '../types'; +import { createCorePageObjects } from '../../page_objects'; + +export const pageObjectsFixture = base.extend({ + pageObjects: async ({ page }, use) => { + const corePageObjects = createCorePageObjects(page); + + await use(corePageObjects); + }, +}); diff --git a/packages/kbn-scout/src/playwright/fixtures/types/index.ts b/packages/kbn-scout/src/playwright/fixtures/types/index.ts new file mode 100644 index 0000000000000..4a23d4c5ce936 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/types/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './test_scope'; +export * from './worker_scope'; diff --git a/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts b/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts new file mode 100644 index 0000000000000..2808381f0f6be --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/types/test_scope.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Page } from 'playwright/test'; +import { PageObjects } from '../../page_objects'; + +export interface ScoutTestFixtures { + browserAuth: LoginFixture; + page: ScoutPage; + pageObjects: PageObjects; +} + +export interface LoginFixture { + loginAsViewer: () => Promise; + loginAsAdmin: () => Promise; + loginAsPrivilegedUser: () => Promise; +} + +export type ScoutPage = Page & { + gotoApp: (appName: string, options?: Parameters[1]) => ReturnType; + testSubj: { + check: (selector: string, options?: Parameters[1]) => ReturnType; + click: (selector: string, options?: Parameters[1]) => ReturnType; + dblclick: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + fill: ( + selector: string, + value: string, + options?: Parameters[2] + ) => ReturnType; + focus: (selector: string, options?: Parameters[1]) => ReturnType; + getAttribute: ( + selector: string, + name: string, + options?: Parameters[2] + ) => ReturnType; + hover: (selector: string, options?: Parameters[1]) => ReturnType; + innerText: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + isEnabled: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + isChecked: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + isHidden: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + locator: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + }; +}; diff --git a/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts new file mode 100644 index 0000000000000..c9424dc0f5970 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { KbnClient, SamlSessionManager } from '@kbn/test'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { Client } from '@elastic/elasticsearch'; +import { LoadActionPerfOptions } from '@kbn/es-archiver'; +import { IndexStats } from '@kbn/es-archiver/src/lib/stats'; + +import { ScoutServerConfig } from '../../../types'; +import { KibanaUrl } from '../../../common/services/kibana_url'; + +interface EsArchiverFixture { + loadIfNeeded: ( + name: string, + performance?: LoadActionPerfOptions | undefined + ) => Promise>; +} + +export interface ScoutWorkerFixtures { + log: ToolingLog; + config: ScoutServerConfig; + kbnUrl: KibanaUrl; + esClient: Client; + kbnClient: KbnClient; + esArchiver: EsArchiverFixture; + samlAuth: SamlSessionManager; +} + +// re-export to import types from '@kbn-scout' +export type { KbnClient, SamlSessionManager } from '@kbn/test'; +export type { ToolingLog } from '@kbn/tooling-log'; +export type { Client } from '@elastic/elasticsearch'; +export type { KibanaUrl } from '../../../common/services/kibana_url'; diff --git a/packages/kbn-scout/src/playwright/fixtures/worker/index.ts b/packages/kbn-scout/src/playwright/fixtures/worker/index.ts new file mode 100644 index 0000000000000..c61d9755c44db --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/worker/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test as base } from '@playwright/test'; + +import { LoadActionPerfOptions } from '@kbn/es-archiver'; +import { + createKbnUrl, + createEsArchiver, + createEsClient, + createKbnClient, + createLogger, + createSamlSessionManager, + createScoutConfig, +} from '../../../common/services'; +import { ScoutWorkerFixtures } from '../types/worker_scope'; +import { ScoutTestOptions } from '../../types'; + +export const scoutWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({ + log: [ + ({}, use) => { + use(createLogger()); + }, + { scope: 'worker' }, + ], + + config: [ + ({ log }, use, testInfo) => { + const configName = 'local'; + const projectUse = testInfo.project.use as ScoutTestOptions; + const serversConfigDir = projectUse.serversConfigDir; + const configInstance = createScoutConfig(serversConfigDir, configName, log); + + use(configInstance); + }, + { scope: 'worker' }, + ], + + kbnUrl: [ + ({ config, log }, use) => { + use(createKbnUrl(config, log)); + }, + { scope: 'worker' }, + ], + + esClient: [ + ({ config, log }, use) => { + use(createEsClient(config, log)); + }, + { scope: 'worker' }, + ], + + kbnClient: [ + ({ log, config }, use) => { + use(createKbnClient(config, log)); + }, + { scope: 'worker' }, + ], + + esArchiver: [ + ({ log, esClient, kbnClient }, use) => { + const esArchiverInstance = createEsArchiver(esClient, kbnClient, log); + // to speedup test execution we only allow to ingest the data indexes and only if index doesn't exist + const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) => + esArchiverInstance!.loadIfNeeded(name, performance); + + use({ loadIfNeeded }); + }, + { scope: 'worker' }, + ], + + samlAuth: [ + ({ log, config }, use) => { + use(createSamlSessionManager(config, log)); + }, + { scope: 'worker' }, + ], +}); diff --git a/packages/kbn-scout/src/playwright/index.ts b/packages/kbn-scout/src/playwright/index.ts new file mode 100644 index 0000000000000..66c80f0068f06 --- /dev/null +++ b/packages/kbn-scout/src/playwright/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { mergeTests } from 'playwright/test'; +import { scoutCoreFixtures } from './fixtures'; + +// Scout core fixtures: worker & test scope +export const test = mergeTests(scoutCoreFixtures); + +export { createPlaywrightConfig } from './config'; +export { createLazyPageObject } from './page_objects/utils'; +export { expect } from './expect'; + +export type { ScoutPlaywrightOptions, ScoutTestOptions } from './types'; +export type { PageObjects } from './page_objects'; +export type { ScoutTestFixtures, ScoutWorkerFixtures, ScoutPage } from './fixtures'; diff --git a/packages/kbn-scout/src/playwright/page_objects/date_picker.ts b/packages/kbn-scout/src/playwright/page_objects/date_picker.ts new file mode 100644 index 0000000000000..08b724a956a3d --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/date_picker.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../fixtures/types'; +import { expect } from '..'; + +export class DatePicker { + constructor(private readonly page: ScoutPage) {} + + async setAbsoluteRange({ from, to }: { from: string; to: string }) { + await this.page.testSubj.click('superDatePickerShowDatesButton'); + // we start with end date + await this.page.testSubj.click('superDatePickerendDatePopoverButton'); + await this.page.testSubj.click('superDatePickerAbsoluteTab'); + const inputFrom = this.page.testSubj.locator('superDatePickerAbsoluteDateInput'); + await inputFrom.clear(); + await inputFrom.fill(to); + await this.page.testSubj.click('parseAbsoluteDateFormat'); + await this.page.testSubj.click('superDatePickerendDatePopoverButton'); + // and later change start date + await this.page.testSubj.click('superDatePickerstartDatePopoverButton'); + await this.page.testSubj.click('superDatePickerAbsoluteTab'); + const inputTo = this.page.testSubj.locator('superDatePickerAbsoluteDateInput'); + await inputTo.clear(); + await inputTo.fill(from); + await this.page.testSubj.click('parseAbsoluteDateFormat'); + await this.page.keyboard.press('Escape'); + + await expect(this.page.testSubj.locator('superDatePickerstartDatePopoverButton')).toHaveText( + from + ); + await expect(this.page.testSubj.locator('superDatePickerendDatePopoverButton')).toHaveText(to); + await this.page.testSubj.click('querySubmitButton'); + } +} diff --git a/packages/kbn-scout/src/playwright/page_objects/discover_app.ts b/packages/kbn-scout/src/playwright/page_objects/discover_app.ts new file mode 100644 index 0000000000000..e4abbf252ae31 --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/discover_app.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../fixtures/types'; + +export class DiscoverApp { + constructor(private readonly page: ScoutPage) {} + + async goto() { + await this.page.gotoApp('discover'); + } +} diff --git a/packages/kbn-scout/src/playwright/page_objects/index.ts b/packages/kbn-scout/src/playwright/page_objects/index.ts new file mode 100644 index 0000000000000..fb90dfea38ff8 --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../fixtures/types'; +import { DatePicker } from './date_picker'; +import { DiscoverApp } from './discover_app'; +import { createLazyPageObject } from './utils'; + +export interface PageObjects { + datePicker: DatePicker; + discover: DiscoverApp; +} + +/** + * Creates a set of core page objects, each lazily instantiated on first access. + * + * @param page - `ScoutPage` instance used for initializing page objects. + * @returns An object containing lazy-loaded core page objects. + */ +export function createCorePageObjects(page: ScoutPage): PageObjects { + return { + datePicker: createLazyPageObject(DatePicker, page), + discover: createLazyPageObject(DiscoverApp, page), + // Add new page objects here + }; +} diff --git a/packages/kbn-scout/src/playwright/page_objects/utils/index.ts b/packages/kbn-scout/src/playwright/page_objects/utils/index.ts new file mode 100644 index 0000000000000..5593a324a274f --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/utils/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../../fixtures/types'; + +/** + * Creates a lazily instantiated proxy for a Page Object class, deferring the creation of the instance until + * a property or method is accessed. It helps avoiding instantiation of page objects that may not be used + * in certain test scenarios. + * + * @param PageObjectClass - The page object class to be instantiated lazily. + * @param scoutPage - ScoutPage instance, that extendes the Playwright `page` fixture and passed to the page object class constructor. + * @param constructorArgs - Additional arguments to be passed to the page object class constructor. + * @returns A proxy object that behaves like an instance of the page object class, instantiating it on demand. + */ +export function createLazyPageObject( + PageObjectClass: new (page: ScoutPage, ...args: any[]) => T, + scoutPage: ScoutPage, + ...constructorArgs: any[] +): T { + let instance: T | null = null; + return new Proxy({} as T, { + get(_, prop: string | symbol) { + if (!instance) { + instance = new PageObjectClass(scoutPage, ...constructorArgs); + } + if (typeof prop === 'symbol' || !(prop in instance)) { + return undefined; + } + return instance[prop as keyof T]; + }, + }); +} diff --git a/packages/kbn-scout/src/playwright/runner/config_validator.ts b/packages/kbn-scout/src/playwright/runner/config_validator.ts new file mode 100644 index 0000000000000..a066a6dfba30c --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/config_validator.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import * as Fs from 'fs'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { PlaywrightTestConfig } from 'playwright/test'; +import path from 'path'; +import { createFlagError } from '@kbn/dev-cli-errors'; +import { ScoutTestOptions, VALID_CONFIG_MARKER } from '../types'; + +export async function validatePlaywrightConfig(configPath: string) { + const fullPath = path.resolve(REPO_ROOT, configPath); + + // Check if the path exists and has a .ts extension + if (!configPath || !Fs.existsSync(fullPath) || !configPath.endsWith('.ts')) { + throw createFlagError( + `Path to a valid TypeScript config file is required: --config ` + ); + } + + // Dynamically import the file to check for a default export + const configModule = await import(fullPath); + const config = configModule.default as PlaywrightTestConfig; + + // Check if the config's 'use' property has the valid marker + if (!config?.use?.[VALID_CONFIG_MARKER]) { + throw createFlagError( + `The config file at "${configPath}" must be created with "createPlaywrightConfig" from '@kbn/scout' package:\n +export default createPlaywrightConfig({ + testDir: './tests', +});` + ); + } + + if (!config.testDir) { + throw createFlagError( + `The config file at "${configPath}" must export a valid Playwright configuration with "testDir" property.` + ); + } +} diff --git a/packages/kbn-scout/src/playwright/runner/flags.ts b/packages/kbn-scout/src/playwright/runner/flags.ts new file mode 100644 index 0000000000000..7d39d821705c1 --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/flags.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { FlagOptions, FlagsReader } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; +import { SERVER_FLAG_OPTIONS, parseServerFlags } from '../../servers'; +import { CliSupportedServerModes } from '../../types'; +import { validatePlaywrightConfig } from './config_validator'; + +export interface RunTestsOptions { + configPath: string; + headed: boolean; + mode: CliSupportedServerModes; + esFrom: 'serverless' | 'source' | 'snapshot' | undefined; + installDir: string | undefined; + logsDir: string | undefined; +} + +export const TEST_FLAG_OPTIONS: FlagOptions = { + ...SERVER_FLAG_OPTIONS, + boolean: [...(SERVER_FLAG_OPTIONS.boolean || []), 'headed'], + string: [...(SERVER_FLAG_OPTIONS.string || []), 'config'], + default: { headed: false }, + help: `${SERVER_FLAG_OPTIONS.help} + --config Playwright config file path + --headed Run Playwright with browser head + `, +}; + +export async function parseTestFlags(flags: FlagsReader) { + const options = parseServerFlags(flags); + const configPath = flags.string('config'); + const headed = flags.boolean('headed'); + + if (!configPath) { + throw createFlagError(`Path to playwright config is required: --config `); + } + + await validatePlaywrightConfig(configPath); + + return { + ...options, + configPath, + headed, + }; +} diff --git a/packages/kbn-scout/src/playwright/runner/index.ts b/packages/kbn-scout/src/playwright/runner/index.ts new file mode 100644 index 0000000000000..2e24f2d2d3039 --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { runTests } from './run_tests'; +export { parseTestFlags, TEST_FLAG_OPTIONS } from './flags'; +export type { RunTestsOptions } from './flags'; diff --git a/packages/kbn-scout/src/playwright/runner/run_tests.ts b/packages/kbn-scout/src/playwright/runner/run_tests.ts new file mode 100644 index 0000000000000..a5d8aa137dbfd --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/run_tests.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { resolve } from 'path'; + +import { ToolingLog } from '@kbn/tooling-log'; +import { withProcRunner } from '@kbn/dev-proc-runner'; +import { getTimeReporter } from '@kbn/ci-stats-reporter'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { runElasticsearch, runKibanaServer } from '../../servers'; +import { loadServersConfig } from '../../config'; +import { silence } from '../../common'; +import { RunTestsOptions } from './flags'; +import { getExtraKbnOpts } from '../../servers/run_kibana_server'; + +export async function runTests(log: ToolingLog, options: RunTestsOptions) { + const runStartTime = Date.now(); + const reportTime = getTimeReporter(log, 'scripts/scout_test'); + + const config = await loadServersConfig(options.mode, log); + const playwrightConfigPath = options.configPath; + + await withProcRunner(log, async (procs) => { + const abortCtrl = new AbortController(); + + const onEarlyExit = (msg: string) => { + log.error(msg); + abortCtrl.abort(); + }; + + let shutdownEs; + + try { + shutdownEs = await runElasticsearch({ + onEarlyExit, + config, + log, + esFrom: options.esFrom, + logsDir: options.logsDir, + }); + + await runKibanaServer({ + procs, + onEarlyExit, + config, + installDir: options.installDir, + extraKbnOpts: getExtraKbnOpts(options.installDir, config.get('serverless')), + }); + + // wait for 5 seconds + await silence(log, 5000); + + // Running 'npx playwright test --config=${playwrightConfigPath}' + await procs.run(`playwright`, { + cmd: resolve(REPO_ROOT, './node_modules/.bin/playwright'), + args: ['test', `--config=${playwrightConfigPath}`, ...(options.headed ? ['--headed'] : [])], + cwd: resolve(REPO_ROOT), + env: { + ...process.env, + }, + wait: true, + }); + } finally { + try { + await procs.stop('kibana'); + } finally { + if (shutdownEs) { + await shutdownEs(); + } + } + } + + reportTime(runStartTime, 'ready', { + success: true, + ...options, + }); + }); +} diff --git a/packages/kbn-scout/src/playwright/types/index.ts b/packages/kbn-scout/src/playwright/types/index.ts new file mode 100644 index 0000000000000..c8d0087d62438 --- /dev/null +++ b/packages/kbn-scout/src/playwright/types/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PlaywrightTestConfig, PlaywrightTestOptions } from 'playwright/test'; + +export type Protocol = 'http' | 'https'; + +export const VALID_CONFIG_MARKER = Symbol('validConfig'); + +export interface ScoutTestOptions extends PlaywrightTestOptions { + serversConfigDir: string; + [VALID_CONFIG_MARKER]: boolean; +} + +export interface ScoutPlaywrightOptions extends Pick { + testDir: string; + workers?: 1 | 2; +} diff --git a/packages/kbn-scout/src/playwright/utils/index.ts b/packages/kbn-scout/src/playwright/utils/index.ts new file mode 100644 index 0000000000000..6100cffc2f2c8 --- /dev/null +++ b/packages/kbn-scout/src/playwright/utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`; diff --git a/packages/kbn-scout/src/servers/flags.ts b/packages/kbn-scout/src/servers/flags.ts new file mode 100644 index 0000000000000..7f372d72e2d7c --- /dev/null +++ b/packages/kbn-scout/src/servers/flags.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { v4 as uuidV4 } from 'uuid'; +import { resolve } from 'path'; +import { FlagsReader, FlagOptions } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { CliSupportedServerModes } from '../types'; + +export type StartServerOptions = ReturnType; + +export const SERVER_FLAG_OPTIONS: FlagOptions = { + string: ['serverless', 'esFrom', 'kibana-install-dir'], + boolean: ['stateful', 'logToFile'], + help: ` + --stateful Start Elasticsearch and Kibana with default ESS configuration + --serverless Start Elasticsearch and Kibana with serverless project configuration: es | oblt | security + --esFrom Build Elasticsearch from source or run snapshot or serverless. Default: $TEST_ES_FROM or "snapshot" + --kibana-install-dir Run Kibana from existing install directory instead of from source + --logToFile Write the log output from Kibana/ES to files instead of to stdout + `, +}; + +export function parseServerFlags(flags: FlagsReader) { + const serverlessType = flags.enum('serverless', ['es', 'oblt', 'security']); + const isStateful = flags.boolean('stateful'); + + if (!(serverlessType || isStateful) || (serverlessType && isStateful)) { + throw createFlagError(`Expected exactly one of --serverless= or --stateful flag`); + } + + const mode: CliSupportedServerModes = serverlessType + ? `serverless=${serverlessType}` + : 'stateful'; + + const esFrom = flags.enum('esFrom', ['source', 'snapshot', 'serverless']); + const installDir = flags.string('kibana-install-dir'); + const logsDir = flags.boolean('logToFile') + ? resolve(REPO_ROOT, 'data/ftr_servers_logs', uuidV4()) + : undefined; + + return { + mode, + esFrom, + installDir, + logsDir, + }; +} diff --git a/packages/kbn-scout/src/servers/index.ts b/packages/kbn-scout/src/servers/index.ts new file mode 100644 index 0000000000000..9d19f18f2974e --- /dev/null +++ b/packages/kbn-scout/src/servers/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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { parseServerFlags, SERVER_FLAG_OPTIONS } from './flags'; +export { startServers } from './start_servers'; +export { runKibanaServer } from './run_kibana_server'; +export { runElasticsearch } from './run_elasticsearch'; + +export type { StartServerOptions } from './flags'; diff --git a/packages/kbn-scout/src/servers/run_elasticsearch.ts b/packages/kbn-scout/src/servers/run_elasticsearch.ts new file mode 100644 index 0000000000000..5406f755f5d72 --- /dev/null +++ b/packages/kbn-scout/src/servers/run_elasticsearch.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Url from 'url'; +import { resolve } from 'path'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { REPO_ROOT } from '@kbn/repo-info'; +import type { ArtifactLicense, ServerlessProjectType } from '@kbn/es'; +import { isServerlessProjectType, extractAndArchiveLogs } from '@kbn/es/src/utils'; +import { createTestEsCluster, esTestConfig } from '@kbn/test'; +import { Config } from '../config'; + +interface RunElasticsearchOptions { + log: ToolingLog; + esFrom?: string; + esServerlessImage?: string; + config: Config; + onEarlyExit?: (msg: string) => void; + logsDir?: string; + name?: string; +} + +type EsConfig = ReturnType; + +function getEsConfig({ + config, + esFrom = config.get('esTestCluster.from'), + esServerlessImage, +}: RunElasticsearchOptions) { + const ssl = !!config.get('esTestCluster.ssl'); + const license: ArtifactLicense = config.get('esTestCluster.license'); + const esArgs: string[] = config.get('esTestCluster.serverArgs'); + const esJavaOpts: string | undefined = config.get('esTestCluster.esJavaOpts'); + const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true'); + + const port: number | undefined = config.get('servers.elasticsearch.port'); + + const password: string | undefined = isSecurityEnabled + ? 'changeme' + : config.get('servers.elasticsearch.password'); + + const dataArchive: string | undefined = config.get('esTestCluster.dataArchive'); + const serverless: boolean = config.get('serverless'); + const files: string[] | undefined = config.get('esTestCluster.files'); + + const esServerlessOptions = serverless + ? getESServerlessOptions(esServerlessImage, config) + : undefined; + + return { + ssl, + license, + esArgs, + esJavaOpts, + isSecurityEnabled, + esFrom, + esServerlessOptions, + port, + password, + dataArchive, + serverless, + files, + }; +} + +export async function runElasticsearch( + options: RunElasticsearchOptions +): Promise<() => Promise> { + const { log, logsDir, name } = options; + const config = getEsConfig(options); + + const node = await startEsNode({ + log, + name: name ?? 'scout', + logsDir, + config, + }); + return async () => { + await node.cleanup(); + await extractAndArchiveLogs({ outputFolder: logsDir, log }); + }; +} + +async function startEsNode({ + log, + name, + config, + onEarlyExit, + logsDir, +}: { + log: ToolingLog; + name: string; + config: EsConfig & { transportPort?: number }; + onEarlyExit?: (msg: string) => void; + logsDir?: string; +}) { + const cluster = createTestEsCluster({ + clusterName: `cluster-${name}`, + esArgs: config.esArgs, + esFrom: config.esFrom, + esServerlessOptions: config.esServerlessOptions, + esJavaOpts: config.esJavaOpts, + license: config.license, + password: config.password, + port: config.port, + ssl: config.ssl, + log, + writeLogsToPath: logsDir ? resolve(logsDir, `es-cluster-${name}.log`) : undefined, + basePath: resolve(REPO_ROOT, '.es'), + nodes: [ + { + name, + dataArchive: config.dataArchive, + }, + ], + transportPort: config.transportPort, + onEarlyExit, + serverless: config.serverless, + files: config.files, + }); + + await cluster.start(); + + return cluster; +} + +interface EsServerlessOptions { + projectType: ServerlessProjectType; + host?: string; + resources: string[]; + kibanaUrl: string; + tag?: string; + image?: string; +} + +function getESServerlessOptions( + esServerlessImageFromArg: string | undefined, + config: Config +): EsServerlessOptions { + const esServerlessImageUrlOrTag = + esServerlessImageFromArg || + esTestConfig.getESServerlessImage() || + (config.has('esTestCluster.esServerlessImage') && + config.get('esTestCluster.esServerlessImage')); + const serverlessResources: string[] = + (config.has('esServerlessOptions.resources') && config.get('esServerlessOptions.resources')) || + []; + const serverlessHost: string | undefined = + config.has('esServerlessOptions.host') && config.get('esServerlessOptions.host'); + + const kbnServerArgs = + (config.has('kbnTestServer.serverArgs') && + (config.get('kbnTestServer.serverArgs') as string[])) || + []; + + const projectType = kbnServerArgs + .filter((arg) => arg.startsWith('--serverless')) + .reduce((acc, arg) => { + const match = arg.match(/--serverless[=\s](\w+)/); + return acc + (match ? match[1] : ''); + }, '') as ServerlessProjectType; + + if (!isServerlessProjectType(projectType)) { + throw new Error(`Unsupported serverless projectType: ${projectType}`); + } + + const commonOptions = { + projectType, + host: serverlessHost, + resources: serverlessResources, + kibanaUrl: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + }; + + if (esServerlessImageUrlOrTag) { + return { + ...commonOptions, + ...(esServerlessImageUrlOrTag.includes(':') + ? { image: esServerlessImageUrlOrTag } + : { tag: esServerlessImageUrlOrTag }), + }; + } + + return commonOptions; +} diff --git a/packages/kbn-scout/src/servers/run_kibana_server.ts b/packages/kbn-scout/src/servers/run_kibana_server.ts new file mode 100644 index 0000000000000..1363b8daaa906 --- /dev/null +++ b/packages/kbn-scout/src/servers/run_kibana_server.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import Path from 'path'; +import Os from 'os'; +import { v4 as uuidv4 } from 'uuid'; +import type { ProcRunner } from '@kbn/dev-proc-runner'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { parseRawFlags, getArgValue, remapPluginPaths, DedicatedTaskRunner } from '@kbn/test'; +import { Config } from '../config'; + +export async function runKibanaServer(options: { + procs: ProcRunner; + config: Config; + installDir?: string; + extraKbnOpts?: string[]; + logsDir?: string; + onEarlyExit?: (msg: string) => void; +}) { + const { config, procs } = options; + const runOptions = options.config.get('kbnTestServer.runOptions'); + const installDir = runOptions.alwaysUseSource ? undefined : options.installDir; + const devMode = !installDir; + const useTaskRunner = options.config.get('kbnTestServer.useDedicatedTaskRunner'); + + const procRunnerOpts = { + cwd: installDir || REPO_ROOT, + cmd: installDir + ? process.platform.startsWith('win') + ? Path.resolve(installDir, 'bin/kibana.bat') + : Path.resolve(installDir, 'bin/kibana') + : process.execPath, + env: { + FORCE_COLOR: 1, + ...process.env, + ...options.config.get('kbnTestServer.env'), + }, + wait: runOptions.wait, + onEarlyExit: options.onEarlyExit, + }; + + const prefixArgs = devMode + ? [Path.relative(procRunnerOpts.cwd, Path.resolve(REPO_ROOT, 'scripts/kibana'))] + : []; + + const buildArgs: string[] = config.get('kbnTestServer.buildArgs') || []; + const sourceArgs: string[] = config.get('kbnTestServer.sourceArgs') || []; + const serverArgs: string[] = config.get('kbnTestServer.serverArgs') || []; + + let kbnFlags = parseRawFlags([ + // When installDir is passed, we run from a built version of Kibana which uses different command line + // arguments. If installDir is not passed, we run from source code. + ...(installDir ? [...buildArgs, ...serverArgs] : [...sourceArgs, ...serverArgs]), + + // We also allow passing in extra Kibana server options, tack those on here so they always take precedence + ...(options.extraKbnOpts ?? []), + ]); + + if (installDir) { + kbnFlags = remapPluginPaths(kbnFlags, installDir); + } + + const mainName = useTaskRunner ? 'kbn-ui' : 'kibana'; + const promises = [ + // main process + procs.run(mainName, { + ...procRunnerOpts, + writeLogsToPath: options.logsDir + ? Path.resolve(options.logsDir, `${mainName}.log`) + : undefined, + args: [ + ...prefixArgs, + ...parseRawFlags([ + ...kbnFlags, + ...(!useTaskRunner + ? [] + : [ + '--node.roles=["ui"]', + `--path.data=${Path.resolve(Os.tmpdir(), `scout-ui-${uuidv4()}`)}`, + ]), + ]), + ], + }), + ]; + + if (useTaskRunner) { + const mainUuid = getArgValue(kbnFlags, 'server.uuid'); + + // dedicated task runner + promises.push( + procs.run('kbn-tasks', { + ...procRunnerOpts, + writeLogsToPath: options.logsDir + ? Path.resolve(options.logsDir, 'kbn-tasks.log') + : undefined, + args: [ + ...prefixArgs, + ...parseRawFlags([ + ...kbnFlags, + `--server.port=${DedicatedTaskRunner.getPort(config.get('servers.kibana.port'))}`, + '--node.roles=["background_tasks"]', + `--path.data=${Path.resolve(Os.tmpdir(), `ftr-task-runner-${uuidv4()}`)}`, + ...(typeof mainUuid === 'string' && mainUuid + ? [`--server.uuid=${DedicatedTaskRunner.getUuid(mainUuid)}`] + : []), + ...(devMode ? ['--no-optimizer'] : []), + ]), + ], + }) + ); + } + + await Promise.all(promises); +} + +export function getExtraKbnOpts(installDir: string | undefined, isServerless: boolean) { + if (installDir) { + return []; + } + + return [ + '--dev', + '--no-dev-config', + '--no-dev-credentials', + isServerless + ? '--server.versioned.versionResolution=newest' + : '--server.versioned.versionResolution=oldest', + ]; +} diff --git a/packages/kbn-scout/src/servers/start_servers.ts b/packages/kbn-scout/src/servers/start_servers.ts new file mode 100644 index 0000000000000..32eb2030c978d --- /dev/null +++ b/packages/kbn-scout/src/servers/start_servers.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import dedent from 'dedent'; + +import { ToolingLog } from '@kbn/tooling-log'; +import { withProcRunner } from '@kbn/dev-proc-runner'; +import { getTimeReporter } from '@kbn/ci-stats-reporter'; +import { runElasticsearch } from './run_elasticsearch'; +import { getExtraKbnOpts, runKibanaServer } from './run_kibana_server'; +import { StartServerOptions } from './flags'; +import { loadServersConfig } from '../config'; +import { silence } from '../common'; + +export async function startServers(log: ToolingLog, options: StartServerOptions) { + const runStartTime = Date.now(); + const reportTime = getTimeReporter(log, 'scripts/scout_start_servers'); + + await withProcRunner(log, async (procs) => { + const config = await loadServersConfig(options.mode, log); + + const shutdownEs = await runElasticsearch({ + config, + log, + esFrom: options.esFrom, + logsDir: options.logsDir, + }); + + await runKibanaServer({ + procs, + config, + installDir: options.installDir, + extraKbnOpts: getExtraKbnOpts(options.installDir, config.get('serverless')), + }); + + reportTime(runStartTime, 'ready', { + success: true, + ...options, + }); + + // wait for 5 seconds of silence before logging the + // success message so that it doesn't get buried + await silence(log, 5000); + + log.success( + '\n\n' + + dedent` + Elasticsearch and Kibana are ready for functional testing. + Use 'npx playwright test --config ' to run tests' + ` + + '\n\n' + ); + + await procs.waitForAllToStop(); + await shutdownEs(); + }); +} diff --git a/packages/kbn-scout/src/types/cli.d.ts b/packages/kbn-scout/src/types/cli.d.ts new file mode 100644 index 0000000000000..9f0d5a2653652 --- /dev/null +++ b/packages/kbn-scout/src/types/cli.d.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type CliSupportedServerModes = + | 'stateful' + | 'serverless=es' + | 'serverless=oblt' + | 'serverless=security'; diff --git a/packages/kbn-scout/src/types/config.d.ts b/packages/kbn-scout/src/types/config.d.ts new file mode 100644 index 0000000000000..14cd27b47fde2 --- /dev/null +++ b/packages/kbn-scout/src/types/config.d.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { UrlParts } from '@kbn/test'; + +export interface ScoutLoaderConfig { + serverless?: boolean; + servers: { + kibana: UrlParts; + elasticsearch: UrlParts; + fleet?: UrlParts; + }; + dockerServers: any; + esTestCluster: { + from: string; + license?: string; + files: string[]; + serverArgs: string[]; + ssl: boolean; + }; + kbnTestServer: { + env?: any; + buildArgs?: string[]; + sourceArgs?: string[]; + serverArgs: string[]; + useDedicatedTastRunner?: boolean; + }; +} diff --git a/packages/kbn-scout/src/types/index.ts b/packages/kbn-scout/src/types/index.ts new file mode 100644 index 0000000000000..811b63fb1aac3 --- /dev/null +++ b/packages/kbn-scout/src/types/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './config'; +export * from './cli'; +export * from './servers'; diff --git a/packages/kbn-scout/src/types/servers.d.ts b/packages/kbn-scout/src/types/servers.d.ts new file mode 100644 index 0000000000000..587e1d213b9ba --- /dev/null +++ b/packages/kbn-scout/src/types/servers.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ServerlessProjectType } from '@kbn/es'; + +export interface ScoutServerConfig { + serverless: boolean; + projectType?: ServerlessProjectType; + isCloud: boolean; + cloudUsersFilePath: string; + hosts: { + kibana: string; + elasticsearch: string; + }; + auth: { + username: string; + password: string; + }; + metadata?: any; +} diff --git a/packages/kbn-scout/tsconfig.json b/packages/kbn-scout/tsconfig.json new file mode 100644 index 0000000000000..35d74c6437618 --- /dev/null +++ b/packages/kbn-scout/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/tooling-log", + "@kbn/dev-cli-runner", + "@kbn/dev-cli-errors", + "@kbn/ci-stats-reporter", + "@kbn/repo-info", + "@kbn/es", + "@kbn/dev-proc-runner", + "@kbn/test", + "@kbn/es-archiver", + "@kbn/dev-utils", + "@kbn/mock-idp-utils", + "@kbn/test-suites-xpack", + "@kbn/test-subj-selector", + ] +} diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx index 08b91ffc1842c..ab851495187c8 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx @@ -20,7 +20,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, - EuiIcon, + EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -110,7 +110,7 @@ export const ConfigSensitiveTextArea: React.FC = ({

{label}

- + ) : ( @@ -228,7 +228,7 @@ export const ConnectorConfigurationField: React.FC{label}

} + label={label} onChange={(event) => { validateAndSetConfigValue(event.target.checked); }} @@ -263,27 +263,21 @@ export const ConnectorConfigurationField: React.FC - -

{label}

-
- - - - - ) : ( -

{label}

- ) - } - onChange={(event) => { - validateAndSetConfigValue(event.target.checked); - }} - /> + + { + validateAndSetConfigValue(event.target.checked); + }} + /> + {tooltip && ( + + + + )} + ); default: diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_form_items.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_form_items.tsx index 8d90aa9a39d94..a53ece70c807a 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_form_items.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_form_items.tsx @@ -9,7 +9,7 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -66,7 +66,9 @@ export const ConnectorConfigurationFormItems: React.FC{label}

- +
+ +
) : ( @@ -77,46 +79,42 @@ export const ConnectorConfigurationFormItems: React.FC - - - { - setConfigEntry(configEntry.key, value); - }} - /> - - + + { + setConfigEntry(configEntry.key, value); + }} + /> + ); } return ( - - - { - setConfigEntry(configEntry.key, value); - }} - /> - - + + { + setConfigEntry(configEntry.key, value); + }} + /> + ); })} diff --git a/packages/kbn-search-index-documents/components/result/result_field.tsx b/packages/kbn-search-index-documents/components/result/result_field.tsx index acd495f71cd44..cfc9263cf87fe 100644 --- a/packages/kbn-search-index-documents/components/result/result_field.tsx +++ b/packages/kbn-search-index-documents/components/result/result_field.tsx @@ -85,6 +85,12 @@ export const ResultField: React.FC = ({ setIsPopoverOpen(!isPopoverOpen)} iconType={iconType || (fieldType ? iconMap[fieldType] : defaultToken)} /> diff --git a/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts index dc2497bb19cec..5152132cda4bb 100644 --- a/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts +++ b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts @@ -291,6 +291,29 @@ export const eventDetailsFormattedFields = [ originalValue: [`{"lon":118.7778,"lat":32.0617}`], values: [`{"lon":118.7778,"lat":32.0617}`], }, + { + category: 'threat', + field: 'threat.enrichments', + isObjectArray: true, + originalValue: [ + '{"matched.field":["matched_field","other_matched_field"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["yourself"],"indicator.type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"]},{"great.field":["grrrrr_2"]}]}', + '{"matched.field":["matched_field_2"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["other_you"],"indicator.type":["custom"],"matched.atomic":["matched_atomic_2"],"lazer":[{"great.field":[{"wowoe":[{"fooooo":["grrrrr"]}],"astring":"cool","aNumber":1,"neat":true}]}]}', + '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["FFEtSYIBZ61VHL7LvV2j"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.architecture"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["x86_64"]}', + '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["CFErSYIBZ61VHL7LIV1N"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + ], + values: [ + '{"matched.field":["matched_field","other_matched_field"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["yourself"],"indicator.type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"]},{"great.field":["grrrrr_2"]}]}', + '{"matched.field":["matched_field_2"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["other_you"],"indicator.type":["custom"],"matched.atomic":["matched_atomic_2"],"lazer":[{"great.field":[{"wowoe":[{"fooooo":["grrrrr"]}],"astring":"cool","aNumber":1,"neat":true}]}]}', + '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["FFEtSYIBZ61VHL7LvV2j"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.architecture"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["x86_64"]}', + '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["CFErSYIBZ61VHL7LIV1N"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', + ], + }, { category: 'threat', field: 'threat.enrichments.matched.field', @@ -376,27 +399,4 @@ export const eventDetailsFormattedFields = [ originalValue: ['FFEtSYIBZ61VHL7LvV2j', 'E1EtSYIBZ61VHL7Ltl3m', 'CFErSYIBZ61VHL7LIV1N'], values: ['FFEtSYIBZ61VHL7LvV2j', 'E1EtSYIBZ61VHL7Ltl3m', 'CFErSYIBZ61VHL7LIV1N'], }, - { - category: 'threat', - field: 'threat.enrichments', - isObjectArray: true, - originalValue: [ - '{"matched.field":["matched_field","other_matched_field"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["yourself"],"indicator.type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"]},{"great.field":["grrrrr_2"]}]}', - '{"matched.field":["matched_field_2"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["other_you"],"indicator.type":["custom"],"matched.atomic":["matched_atomic_2"],"lazer":[{"great.field":[{"wowoe":[{"fooooo":["grrrrr"]}],"astring":"cool","aNumber":1,"neat":true}]}]}', - '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["FFEtSYIBZ61VHL7LvV2j"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.architecture"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["x86_64"]}', - '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["CFErSYIBZ61VHL7LIV1N"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - ], - values: [ - '{"matched.field":["matched_field","other_matched_field"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["yourself"],"indicator.type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"]},{"great.field":["grrrrr_2"]}]}', - '{"matched.field":["matched_field_2"],"indicator.first_seen":["2021-02-22T17:29:25.195Z"],"indicator.provider":["other_you"],"indicator.type":["custom"],"matched.atomic":["matched_atomic_2"],"lazer":[{"great.field":[{"wowoe":[{"fooooo":["grrrrr"]}],"astring":"cool","aNumber":1,"neat":true}]}]}', - '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["FFEtSYIBZ61VHL7LvV2j"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.architecture"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["x86_64"]}', - '{"matched.field":["host.name"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["E1EtSYIBZ61VHL7Ltl3m"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - '{"matched.field":["host.hostname"],"matched.index":["im"],"matched.type":["indicator_match_rule"],"matched.id":["CFErSYIBZ61VHL7LIV1N"],"matched.atomic":["MacBook-Pro-de-Gloria.local"]}', - ], - }, ]; diff --git a/packages/kbn-server-route-repository-client/src/create_repository_client.ts b/packages/kbn-server-route-repository-client/src/create_repository_client.ts index 015db2b9948d8..325f484103d09 100644 --- a/packages/kbn-server-route-repository-client/src/create_repository_client.ts +++ b/packages/kbn-server-route-repository-client/src/create_repository_client.ts @@ -15,12 +15,12 @@ import { } from '@kbn/server-route-repository-utils'; import { httpResponseIntoObservable } from '@kbn/sse-utils-client'; import { from } from 'rxjs'; -import { HttpFetchOptions, HttpFetchQuery, HttpResponse } from '@kbn/core-http-browser'; +import { HttpFetchQuery, HttpResponse } from '@kbn/core-http-browser'; import { omit } from 'lodash'; export function createRepositoryClient< TRepository extends ServerRouteRepository, - TClientOptions extends HttpFetchOptions = {} + TClientOptions extends Record = {} >(core: CoreStart | CoreSetup): RouteRepositoryClient { const fetch = ( endpoint: string, diff --git a/packages/kbn-server-route-repository-utils/index.ts b/packages/kbn-server-route-repository-utils/index.ts index 14322f9b64c8f..a1e3ec45bd6f7 100644 --- a/packages/kbn-server-route-repository-utils/index.ts +++ b/packages/kbn-server-route-repository-utils/index.ts @@ -18,7 +18,6 @@ export type { EndpointOf, ReturnOf, RouteRepositoryClient, - RouteState, ClientRequestParamsOf, DecodedRequestParamsOf, ServerRouteRepository, diff --git a/packages/kbn-server-route-repository-utils/src/parse_endpoint.ts b/packages/kbn-server-route-repository-utils/src/parse_endpoint.ts index e16590c9a666f..b81b3285ec8f5 100644 --- a/packages/kbn-server-route-repository-utils/src/parse_endpoint.ts +++ b/packages/kbn-server-route-repository-utils/src/parse_endpoint.ts @@ -7,22 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; +import type { RouteMethod } from '@kbn/core-http-server'; + +const validMethods: RouteMethod[] = ['delete', 'get', 'patch', 'post', 'put']; export function parseEndpoint(endpoint: string) { const parts = endpoint.split(' '); - const method = parts[0].trim().toLowerCase() as Method; + const method = parts[0].trim().toLowerCase() as Exclude; const pathname = parts[1].trim(); const version = parts[2]?.trim(); - if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) { + if (!validMethods.includes(method)) { throw new Error(`Endpoint ${endpoint} was not prefixed with a valid HTTP method`); } - if (!version && pathname.startsWith('/api')) { - throw new Error(`Missing version for public endpoint ${endpoint}`); - } - return { method, pathname, version }; } diff --git a/packages/kbn-server-route-repository-utils/src/typings.ts b/packages/kbn-server-route-repository-utils/src/typings.ts index 35a2f41054c99..5db4a87b8b326 100644 --- a/packages/kbn-server-route-repository-utils/src/typings.ts +++ b/packages/kbn-server-route-repository-utils/src/typings.ts @@ -8,7 +8,7 @@ */ import type { HttpFetchOptions } from '@kbn/core-http-browser'; -import type { IKibanaResponse } from '@kbn/core-http-server'; +import type { IKibanaResponse, RouteAccess, RouteSecurity } from '@kbn/core-http-server'; import type { KibanaRequest, KibanaResponseFactory, @@ -22,7 +22,7 @@ import { z } from '@kbn/zod'; import * as t from 'io-ts'; import { Observable } from 'rxjs'; import { Readable } from 'stream'; -import { RequiredKeys, ValuesType } from 'utility-types'; +import { Required, RequiredKeys, ValuesType } from 'utility-types'; type MaybeOptional }> = RequiredKeys< T['params'] @@ -50,24 +50,37 @@ export type ZodParamsObject = z.ZodObject<{ export type IoTsParamsObject = WithoutIncompatibleMethods>; export type RouteParamsRT = IoTsParamsObject | ZodParamsObject; +export type ServerRouteHandlerResources = Record; -export interface RouteState { - [endpoint: string]: ServerRoute; +export interface ServerRouteCreateOptions { + [x: string]: any; } -export type ServerRouteHandlerResources = Record; -export type ServerRouteCreateOptions = Record; +type RouteMethodOf = TEndpoint extends `${infer TRouteMethod} ${string}` + ? Lowercase extends RouteMethod + ? Lowercase + : never + : never; -type ValidateEndpoint = string extends TEndpoint +type IsPublicEndpoint< + TEndpoint extends string, + TRouteAccess extends RouteAccess | undefined +> = TRouteAccess extends 'public' ? true - : TEndpoint extends `${string} ${string} ${string}` + : TRouteAccess extends 'internal' + ? false + : TEndpoint extends `${string} /api${string}` ? true - : TEndpoint extends `${string} ${infer TPathname}` - ? TPathname extends `/internal/${string}` - ? true - : false : false; +type IsVersionSpecified = + TEndpoint extends `${string} ${string} ${string}` ? true : false; + +type ValidateEndpoint< + TEndpoint extends string, + TRouteAccess extends RouteAccess | undefined +> = IsPublicEndpoint extends true ? IsVersionSpecified : true; + type IsAny = 1 | 0 extends (T extends never ? 1 : 0) ? true : false; // this ensures only plain objects can be returned, if it's not one @@ -127,17 +140,27 @@ type ServerRouteHandler< export type CreateServerRouteFactory< TRouteHandlerResources extends ServerRouteHandlerResources, - TRouteCreateOptions extends ServerRouteCreateOptions + TRouteCreateOptions extends DefaultRouteCreateOptions | undefined > = < TEndpoint extends string, TReturnType extends ServerRouteHandlerReturnType, - TRouteParamsRT extends RouteParamsRT | undefined = undefined + TRouteParamsRT extends RouteParamsRT | undefined = undefined, + TRouteAccess extends RouteAccess | undefined = undefined >( options: { - endpoint: ValidateEndpoint extends true ? TEndpoint : never; + endpoint: ValidateEndpoint extends true ? TEndpoint : never; handler: ServerRouteHandler; params?: TRouteParamsRT; - } & TRouteCreateOptions + security?: RouteSecurity; + } & Required< + { + options?: (TRouteCreateOptions extends DefaultRouteCreateOptions ? TRouteCreateOptions : {}) & + RouteConfigOptions> & { + access?: TRouteAccess; + }; + }, + RequiredKeys extends never ? never : 'options' + > ) => Record< TEndpoint, ServerRoute< @@ -154,16 +177,17 @@ export type ServerRoute< TRouteParamsRT extends RouteParamsRT | undefined, TRouteHandlerResources extends ServerRouteHandlerResources, TReturnType extends ServerRouteHandlerReturnType, - TRouteCreateOptions extends ServerRouteCreateOptions + TRouteCreateOptions extends DefaultRouteCreateOptions | undefined > = { endpoint: TEndpoint; handler: ServerRouteHandler; -} & TRouteCreateOptions & - (TRouteParamsRT extends RouteParamsRT ? { params: TRouteParamsRT } : {}); + security?: RouteSecurity; +} & (TRouteParamsRT extends RouteParamsRT ? { params: TRouteParamsRT } : {}) & + (TRouteCreateOptions extends DefaultRouteCreateOptions ? { options: TRouteCreateOptions } : {}); export type ServerRouteRepository = Record< string, - ServerRoute> + ServerRoute >; type ClientRequestParamsOfType = @@ -194,13 +218,7 @@ export type EndpointOf = export type ReturnOf< TServerRouteRepository extends ServerRouteRepository, TEndpoint extends keyof TServerRouteRepository -> = TServerRouteRepository[TEndpoint] extends ServerRoute< - any, - any, - any, - infer TReturnType, - ServerRouteCreateOptions -> +> = TServerRouteRepository[TEndpoint] extends ServerRoute ? TReturnType extends IKibanaResponse ? TWrappedResponseType : TReturnType @@ -209,13 +227,7 @@ export type ReturnOf< export type DecodedRequestParamsOf< TServerRouteRepository extends ServerRouteRepository, TEndpoint extends keyof TServerRouteRepository -> = TServerRouteRepository[TEndpoint] extends ServerRoute< - any, - infer TRouteParamsRT, - any, - any, - ServerRouteCreateOptions -> +> = TServerRouteRepository[TEndpoint] extends ServerRoute ? TRouteParamsRT extends RouteParamsRT ? DecodedRequestParamsOfType : {} @@ -229,7 +241,7 @@ export type ClientRequestParamsOf< infer TRouteParamsRT, any, any, - ServerRouteCreateOptions + ServerRouteCreateOptions | undefined > ? TRouteParamsRT extends RouteParamsRT ? ClientRequestParamsOfType @@ -249,13 +261,17 @@ export interface RouteRepositoryClient< fetch>( endpoint: TEndpoint, ...args: MaybeOptionalArgs< - ClientRequestParamsOf & TAdditionalClientOptions + ClientRequestParamsOf & + TAdditionalClientOptions & + HttpFetchOptions > ): Promise>; stream>( endpoint: TEndpoint, ...args: MaybeOptionalArgs< - ClientRequestParamsOf & TAdditionalClientOptions + ClientRequestParamsOf & + TAdditionalClientOptions & + HttpFetchOptions > ): ReturnOf extends Observable ? TReturnType extends ServerSentEvent @@ -276,6 +292,4 @@ export interface DefaultRouteHandlerResources extends CoreRouteHandlerResources logger: Logger; } -export interface DefaultRouteCreateOptions { - options?: RouteConfigOptions; -} +export type DefaultRouteCreateOptions = RouteConfigOptions>; diff --git a/packages/kbn-server-route-repository/index.ts b/packages/kbn-server-route-repository/index.ts index a922c2ebb1e1f..16958e744417b 100644 --- a/packages/kbn-server-route-repository/index.ts +++ b/packages/kbn-server-route-repository/index.ts @@ -23,7 +23,6 @@ export type { ServerRouteRepository, ServerRoute, RouteParamsRT, - RouteState, DefaultRouteCreateOptions, DefaultRouteHandlerResources, IoTsParamsObject, diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts index be375bd069480..34fc375b0d9ab 100644 --- a/packages/kbn-server-route-repository/src/create_server_route_factory.ts +++ b/packages/kbn-server-route-repository/src/create_server_route_factory.ts @@ -8,16 +8,17 @@ */ import type { - DefaultRouteCreateOptions, DefaultRouteHandlerResources, - ServerRouteCreateOptions, ServerRouteHandlerResources, } from '@kbn/server-route-repository-utils'; -import type { CreateServerRouteFactory } from '@kbn/server-route-repository-utils/src/typings'; +import type { + CreateServerRouteFactory, + DefaultRouteCreateOptions, +} from '@kbn/server-route-repository-utils/src/typings'; export function createServerRouteFactory< TRouteHandlerResources extends ServerRouteHandlerResources = DefaultRouteHandlerResources, - TRouteCreateOptions extends ServerRouteCreateOptions = DefaultRouteCreateOptions + TRouteCreateOptions extends DefaultRouteCreateOptions | undefined = undefined >(): CreateServerRouteFactory { return (route) => ({ [route.endpoint]: route } as any); } diff --git a/packages/kbn-server-route-repository/src/register_routes.test.ts b/packages/kbn-server-route-repository/src/register_routes.test.ts index 249f81df3a8a9..b13592c57ba59 100644 --- a/packages/kbn-server-route-repository/src/register_routes.test.ts +++ b/packages/kbn-server-route-repository/src/register_routes.test.ts @@ -15,6 +15,7 @@ import { NEVER } from 'rxjs'; import * as makeZodValidationObject from './make_zod_validation_object'; import { registerRoutes } from './register_routes'; import { passThroughValidationObject, noParamsValidationObject } from './validation_objects'; +import { ServerRouteRepository } from '@kbn/server-route-repository-utils'; describe('registerRoutes', () => { const post = jest.fn(); @@ -54,44 +55,82 @@ describe('registerRoutes', () => { 'POST /internal/route': { endpoint: 'POST /internal/route', handler: jest.fn(), - options: { - internal: true, - }, }, 'POST /api/public_route version': { endpoint: 'POST /api/public_route version', handler: jest.fn(), + }, + 'POST /api/internal_but_looks_like_public version': { + endpoint: 'POST /api/internal_but_looks_like_public version', options: { - public: true, + access: 'internal', }, + handler: jest.fn(), }, - }); + 'POST /internal/route_with_security': { + endpoint: `POST /internal/route_with_security`, + handler: jest.fn(), + security: { + authz: { + enabled: false, + reason: 'whatever', + }, + }, + }, + 'POST /api/route_with_security version': { + endpoint: `POST /api/route_with_security version`, + handler: jest.fn(), + security: { + authz: { + enabled: false, + reason: 'whatever', + }, + }, + }, + } satisfies ServerRouteRepository); expect(createRouter).toHaveBeenCalledTimes(1); - expect(post).toHaveBeenCalledTimes(1); - const [internalRoute] = post.mock.calls[0]; expect(internalRoute.path).toEqual('/internal/route'); expect(internalRoute.options).toEqual({ - internal: true, + access: 'internal', }); expect(internalRoute.validate).toEqual(noParamsValidationObject); - expect(postWithVersion).toHaveBeenCalledTimes(1); + const [internalRouteWithSecurity] = post.mock.calls[1]; + + expect(internalRouteWithSecurity.path).toEqual('/internal/route_with_security'); + expect(internalRouteWithSecurity.security).toEqual({ + authz: { + enabled: false, + reason: 'whatever', + }, + }); + const [publicRoute] = postWithVersion.mock.calls[0]; expect(publicRoute.path).toEqual('/api/public_route'); - expect(publicRoute.options).toEqual({ - public: true, - }); expect(publicRoute.access).toEqual('public'); - expect(postAddVersion).toHaveBeenCalledTimes(1); + const [apiInternalRoute] = postWithVersion.mock.calls[1]; + expect(apiInternalRoute.path).toEqual('/api/internal_but_looks_like_public'); + expect(apiInternalRoute.access).toEqual('internal'); + const [versionedRoute] = postAddVersion.mock.calls[0]; expect(versionedRoute.version).toEqual('version'); expect(versionedRoute.validate).toEqual({ request: noParamsValidationObject, }); + + const [publicRouteWithSecurity] = postWithVersion.mock.calls[2]; + + expect(publicRouteWithSecurity.path).toEqual('/api/route_with_security'); + expect(publicRouteWithSecurity.security).toEqual({ + authz: { + enabled: false, + reason: 'whatever', + }, + }); }); it('does not allow any params if no schema is provided', () => { diff --git a/packages/kbn-server-route-repository/src/register_routes.ts b/packages/kbn-server-route-repository/src/register_routes.ts index 5e0fa51a4544f..6201ffcd869ea 100644 --- a/packages/kbn-server-route-repository/src/register_routes.ts +++ b/packages/kbn-server-route-repository/src/register_routes.ts @@ -15,19 +15,20 @@ import { isKibanaResponse } from '@kbn/core-http-server'; import type { CoreSetup } from '@kbn/core-lifecycle-server'; import type { Logger } from '@kbn/logging'; import { + DefaultRouteCreateOptions, + RouteParamsRT, ServerRoute, - ServerRouteCreateOptions, ZodParamsObject, parseEndpoint, } from '@kbn/server-route-repository-utils'; +import { ServerSentEvent } from '@kbn/sse-utils'; import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; import { isZod } from '@kbn/zod'; -import { merge } from 'lodash'; +import { merge, omit } from 'lodash'; import { Observable, isObservable } from 'rxjs'; -import { ServerSentEvent } from '@kbn/sse-utils'; -import { passThroughValidationObject, noParamsValidationObject } from './validation_objects'; -import { validateAndDecodeParams } from './validate_and_decode_params'; import { makeZodValidationObject } from './make_zod_validation_object'; +import { validateAndDecodeParams } from './validate_and_decode_params'; +import { noParamsValidationObject, passThroughValidationObject } from './validation_objects'; const CLIENT_CLOSED_REQUEST = { statusCode: 499, @@ -43,7 +44,7 @@ export function registerRoutes>({ dependencies, }: { core: CoreSetup; - repository: Record>; + repository: Record>; logger: Logger; dependencies: TDependencies; }) { @@ -52,7 +53,11 @@ export function registerRoutes>({ const router = core.http.createRouter(); routes.forEach((route) => { - const { params, endpoint, options, handler } = route; + const { endpoint, handler, security } = route; + + const params = 'params' in route ? route.params : undefined; + + const options: DefaultRouteCreateOptions = 'options' in route ? route.options : {}; const { method, pathname, version } = parseEndpoint(endpoint); @@ -137,11 +142,18 @@ export function registerRoutes>({ validationObject = passThroughValidationObject; } + const access = options?.access ?? (pathname.startsWith('/internal/') ? 'internal' : 'public'); + if (!version) { router[method]( { path: pathname, - options, + // @ts-expect-error we are essentially calling multiple methods at the same type so TS gets confused + options: { + ...options, + access, + }, + security, validate: validationObject, }, wrappedHandler @@ -149,8 +161,10 @@ export function registerRoutes>({ } else { router.versioned[method]({ path: pathname, - access: pathname.startsWith('/internal/') ? 'internal' : 'public', - options, + access, + // @ts-expect-error we are essentially calling multiple methods at the same type so TS gets confused + options: omit(options, 'access', 'description', 'summary', 'deprecated', 'discontinued'), + security, }).addVersion( { version, diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts index acef5a524eb6b..6b099c158f07f 100644 --- a/packages/kbn-server-route-repository/src/test_types.ts +++ b/packages/kbn-server-route-repository/src/test_types.ts @@ -83,32 +83,44 @@ createServerRouteFactory<{ context: { getSpaceId: () => string } }, {}>()({ }); // Create options are available when registering a route. -createServerRouteFactory<{}, { options: { tags: string[] } }>()({ +createServerRouteFactory<{}, {}>()({ endpoint: 'GET /internal/endpoint_with_params', params: t.type({ path: t.type({ serviceName: t.string, }), }), - options: { - tags: [], - }, handler: async (resources) => { assertType<{ params: { path: { serviceName: string } } }>(resources); }, }); // Public APIs should be versioned -createServerRouteFactory<{}, { options: { tags: string[] } }>()({ +createServerRouteFactory<{}, { tags: string[] }>()({ // @ts-expect-error + endpoint: 'GET /api/endpoint_with_params', + tags: [], + handler: async (resources) => {}, +}); + +// `access` is respected +createServerRouteFactory<{}, { tags: string[] }>()({ endpoint: 'GET /api/endpoint_with_params', options: { tags: [], + access: 'internal', }, handler: async (resources) => {}, }); -createServerRouteFactory<{}, { options: { tags: string[] } }>()({ +// specifying additional options makes them required +// @ts-expect-error +createServerRouteFactory<{}, { tags: string[] }>()({ + endpoint: 'GET /api/endpoint_with_params 2023-10-31', + handler: async (resources) => {}, +}); + +createServerRouteFactory<{}, { tags: string[] }>()({ endpoint: 'GET /api/endpoint_with_params 2023-10-31', options: { tags: [], diff --git a/packages/kbn-test/index.ts b/packages/kbn-test/index.ts index 57d9c767827df..bd196e27c8cb0 100644 --- a/packages/kbn-test/index.ts +++ b/packages/kbn-test/index.ts @@ -14,14 +14,23 @@ export { startServersCli, startServers } from './src/functional_tests/start_serv // @internal export { runTestsCli, runTests } from './src/functional_tests/run_tests'; +export { + runElasticsearch, + runKibanaServer, + parseRawFlags, + getArgValue, + remapPluginPaths, + getKibanaCliArg, + getKibanaCliLoggers, +} from './src/functional_tests/lib'; + +export { initLogsDir } from './src/functional_tests/lib'; export { SamlSessionManager, type SamlSessionManagerOptions, type HostOptions, type GetCookieOptions, } from './src/auth'; -export { runElasticsearch, runKibanaServer } from './src/functional_tests/lib'; -export { getKibanaCliArg, getKibanaCliLoggers } from './src/functional_tests/lib/kibana_cli_args'; export type { CreateTestEsClusterOptions, @@ -38,6 +47,7 @@ export { } from './src/es'; export { kbnTestConfig } from './kbn_test_config'; +export type { UrlParts } from './kbn_test_config'; export { kibanaServerTestUser, diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index 6e781df0a0ea3..6c5641cbe8aab 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -16,6 +16,7 @@ export { Lifecycle, LifecyclePhase, runCheckFtrConfigsCli, + DedicatedTaskRunner, } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts b/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts index f737e380267db..5808c88901b11 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts @@ -50,6 +50,11 @@ export async function runCheckFtrConfigsCli() { return false; } + // playwright config files + if (file.match(/\/ui_tests\/*playwright*.config.ts$/)) { + return false; + } + if (!file.match(/(test|e2e).*config[^\/]*\.(t|j)s$/)) { return false; } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_tests.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_tests.ts index c84d78fe1d20a..2303704ea43fa 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_tests.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_tests.ts @@ -57,7 +57,11 @@ export const loadTests = ({ updateBaselines, }; - decorateSnapshotUi({ lifecycle, updateSnapshots, isCi: !!process.env.CI }); + decorateSnapshotUi({ + lifecycle, + updateSnapshots, + isCi: !!process.env.CI, + }); function loadTestFile(path: string) { if (typeof path !== 'string' || !isAbsolute(path)) { diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts index 78e2b8d7e680c..3d00eae7f22ff 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts @@ -33,11 +33,13 @@ const globalState: { registered: boolean; currentTest: Test | null; snapshotStates: Record; + deploymentAgnostic: boolean; } = { updateSnapshot: 'none', registered: false, currentTest: null, snapshotStates: {}, + deploymentAgnostic: false, }; const modifyStackTracePrepareOnce = once(() => { @@ -125,7 +127,7 @@ export function decorateSnapshotUi({ const snapshotState = globalState.snapshotStates[file]; if (snapshotState && !test.isPassed()) { - snapshotState.markSnapshotsAsCheckedForTest(test.fullTitle()); + snapshotState.markSnapshotsAsCheckedForTest(getTestTitle(test)); } }); @@ -194,7 +196,7 @@ export function expectSnapshot(received: any) { const context: SnapshotContext = { snapshotState, - currentTestName: test.fullTitle(), + currentTestName: getTestTitle(test), }; return { @@ -204,6 +206,18 @@ export function expectSnapshot(received: any) { }; } +function getTestTitle(test: Test) { + return ( + test + .fullTitle() + // remove deployment type from test title so that a single snapshot can be used for all deployment types + .replace( + /^(Serverless|Stateful)\s+([^\-]+)\s*-?\s*Deployment-agnostic/g, + 'Deployment-agnostic' + ) + ); +} + function expectToMatchSnapshot(snapshotContext: SnapshotContext, received: any) { const matcher = toMatchSnapshot.bind(snapshotContext as any); const result = matcher(received) as SyncExpectationResult; diff --git a/packages/kbn-test/src/functional_tests/lib/index.ts b/packages/kbn-test/src/functional_tests/lib/index.ts index 003a675d8421d..23c6ec8331602 100644 --- a/packages/kbn-test/src/functional_tests/lib/index.ts +++ b/packages/kbn-test/src/functional_tests/lib/index.ts @@ -10,3 +10,11 @@ export { runKibanaServer } from './run_kibana_server'; export { runElasticsearch } from './run_elasticsearch'; export * from './run_ftr'; +export { + parseRawFlags, + getArgValue, + remapPluginPaths, + getKibanaCliArg, + getKibanaCliLoggers, +} from './kibana_cli_args'; +export { initLogsDir } from './logs_dir'; diff --git a/packages/kbn-ts-projects/ts_project.ts b/packages/kbn-ts-projects/ts_project.ts index 22c28931da144..4870bc7d0e95b 100644 --- a/packages/kbn-ts-projects/ts_project.ts +++ b/packages/kbn-ts-projects/ts_project.ts @@ -14,6 +14,7 @@ import { REPO_ROOT } from '@kbn/repo-info'; import { makeMatcher } from '@kbn/picomatcher'; import { type Package, findPackageForPath, getRepoRelsSync } from '@kbn/repo-packages'; import { createFailError } from '@kbn/dev-cli-errors'; +import { readPackageJson } from '@kbn/repo-packages'; import { readTsConfig, parseTsConfig, TsConfig } from './ts_configfile'; @@ -151,6 +152,8 @@ export class TsProject { public readonly directory: string; /** the package this tsconfig file is within, if any */ public readonly pkg?: Package; + /** the package is esm or not */ + public readonly isEsm?: boolean; /** * if this project is within a package then this will * be set to the import request that maps to the root of this project @@ -187,6 +190,7 @@ export class TsProject { : undefined; this._disableTypeCheck = !!opts?.disableTypeCheck; + this.isEsm = readPackageJson(`${this.dir}/package.json`)?.type === 'module'; } private _name: string | undefined; diff --git a/packages/kbn-typed-react-router-config/index.ts b/packages/kbn-typed-react-router-config/index.ts index 3075cb48a951b..6aff73c55e6c3 100644 --- a/packages/kbn-typed-react-router-config/index.ts +++ b/packages/kbn-typed-react-router-config/index.ts @@ -17,3 +17,4 @@ export * from './src/use_match_routes'; export * from './src/use_params'; export * from './src/use_router'; export * from './src/use_route_path'; +export * from './src/breadcrumbs'; diff --git a/packages/kbn-typed-react-router-config/kibana.jsonc b/packages/kbn-typed-react-router-config/kibana.jsonc index 37c95108427ee..2f2bda611d6b3 100644 --- a/packages/kbn-typed-react-router-config/kibana.jsonc +++ b/packages/kbn-typed-react-router-config/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-common", + "type": "shared-browser", "id": "@kbn/typed-react-router-config", "owner": [ "@elastic/obs-knowledge-team", diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/breadcrumb.tsx b/packages/kbn-typed-react-router-config/src/breadcrumbs/breadcrumb.tsx new file mode 100644 index 0000000000000..94a32c76c21f2 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/breadcrumb.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { RequiredKeys } from 'utility-types'; +import { useRouterBreadcrumb } from './use_router_breadcrumb'; +import { PathsOf, RouteMap, TypeOf } from '../types'; + +type AsParamsProps> = RequiredKeys extends never + ? {} + : { params: TObject }; + +export type RouterBreadcrumb = < + TRoutePath extends PathsOf +>({}: { + title: string; + children: React.ReactNode; + path: TRoutePath; +} & AsParamsProps>) => React.ReactElement; + +export function RouterBreadcrumb< + TRouteMap extends RouteMap, + TRoutePath extends PathsOf +>({ + title, + path, + params, + children, +}: { + title: string; + path: TRoutePath; + children: React.ReactElement; + params?: Record; +}) { + useRouterBreadcrumb( + () => ({ + title, + path, + params, + }), + [] + ); + + return children; +} diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/context.tsx b/packages/kbn-typed-react-router-config/src/breadcrumbs/context.tsx new file mode 100644 index 0000000000000..21d6a30567b18 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/context.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public'; +import { compact, isEqual } from 'lodash'; +import React, { createContext, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useBreadcrumbs } from './use_breadcrumbs'; +import { + PathsOf, + Route, + RouteMap, + RouteMatch, + TypeAsArgs, + TypeAsParams, + TypeOf, + useMatchRoutes, + useRouter, +} from '../..'; + +export type Breadcrumb< + TRouteMap extends RouteMap = RouteMap, + TPath extends PathsOf = PathsOf +> = { + title: string; + path: TPath; +} & TypeAsParams>; + +interface BreadcrumbApi { + set>( + route: Route, + breadcrumb: Array> + ): void; + unset(route: Route): void; + getBreadcrumbs(matches: RouteMatch[]): Array>>; +} + +export const BreadcrumbsContext = createContext(undefined); + +export function BreadcrumbsContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [, forceUpdate] = useState({}); + + const breadcrumbs = useMemo(() => { + return new Map>>(); + }, []); + + const history = useHistory() as ScopedHistory; + + const router = useRouter(); + + const matches: RouteMatch[] = useMatchRoutes(); + + const api = useMemo>( + () => ({ + set(route, breadcrumb) { + if (!isEqual(breadcrumbs.get(route), breadcrumb)) { + breadcrumbs.set(route, breadcrumb); + forceUpdate({}); + } + }, + unset(route) { + if (breadcrumbs.has(route)) { + breadcrumbs.delete(route); + forceUpdate({}); + } + }, + getBreadcrumbs(currentMatches: RouteMatch[]) { + return compact( + currentMatches.flatMap((match) => { + const breadcrumb = breadcrumbs.get(match.route); + + return breadcrumb; + }) + ); + }, + }), + [breadcrumbs] + ); + + const formattedBreadcrumbs: ChromeBreadcrumb[] = api + .getBreadcrumbs(matches) + .map((breadcrumb, index, array) => { + return { + text: breadcrumb.title, + ...(index === array.length - 1 + ? {} + : { + href: history.createHref({ + pathname: router.link( + breadcrumb.path, + ...(('params' in breadcrumb ? [breadcrumb.params] : []) as TypeAsArgs< + TypeOf, false> + >) + ), + }), + }), + }; + }); + + useBreadcrumbs(formattedBreadcrumbs); + + return {children}; +} diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/create_router_breadcrumb_component.tsx b/packages/kbn-typed-react-router-config/src/breadcrumbs/create_router_breadcrumb_component.tsx new file mode 100644 index 0000000000000..04c8cf4e9d245 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/create_router_breadcrumb_component.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { RouteMap } from '../types'; +import { RouterBreadcrumb } from './breadcrumb'; + +export function createRouterBreadcrumbComponent< + TRouteMap extends RouteMap +>(): RouterBreadcrumb { + return RouterBreadcrumb as RouterBreadcrumb; +} diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/index.tsx b/packages/kbn-typed-react-router-config/src/breadcrumbs/index.tsx new file mode 100644 index 0000000000000..721ba433cc01b --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { createRouterBreadcrumbComponent } from './create_router_breadcrumb_component'; +export { createUseBreadcrumbs } from './use_router_breadcrumb'; +export { BreadcrumbsContextProvider } from './context'; diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/use_breadcrumbs.ts b/packages/kbn-typed-react-router-config/src/breadcrumbs/use_breadcrumbs.ts new file mode 100644 index 0000000000000..3d63c8a0f27d7 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/use_breadcrumbs.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; +import { ApplicationStart, ChromeBreadcrumb, ChromeStart } from '@kbn/core/public'; +import { MouseEvent, useEffect, useMemo } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; + +function addClickHandlers( + breadcrumbs: ChromeBreadcrumb[], + navigateToHref?: (url: string) => Promise +) { + return breadcrumbs.map((bc) => ({ + ...bc, + ...(bc.href + ? { + onClick: (event: MouseEvent) => { + if (navigateToHref && bc.href) { + event.preventDefault(); + navigateToHref(bc.href); + } + }, + } + : {}), + })); +} + +function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) { + return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse(); +} + +export const useBreadcrumbs = ( + extraCrumbs: ChromeBreadcrumb[], + options?: { + app?: { id: string; label: string }; + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension; + serverless?: ServerlessPluginStart; + } +) => { + const { app, breadcrumbsAppendExtension, serverless } = options ?? {}; + + const { + services: { + chrome: { docTitle, setBreadcrumbs: chromeSetBreadcrumbs, setBreadcrumbsAppendExtension }, + application: { getUrlForApp, navigateToUrl }, + }, + } = useKibana<{ + application: ApplicationStart; + chrome: ChromeStart; + }>(); + + const setTitle = docTitle.change; + const appPath = getUrlForApp(app?.id ?? 'observability-overview') ?? ''; + + const setBreadcrumbs = useMemo( + () => serverless?.setBreadcrumbs ?? chromeSetBreadcrumbs, + [serverless, chromeSetBreadcrumbs] + ); + + useEffect(() => { + if (breadcrumbsAppendExtension) { + setBreadcrumbsAppendExtension(breadcrumbsAppendExtension); + } + return () => { + if (breadcrumbsAppendExtension) { + setBreadcrumbsAppendExtension(undefined); + } + }; + }, [breadcrumbsAppendExtension, setBreadcrumbsAppendExtension]); + + useEffect(() => { + const breadcrumbs = serverless + ? extraCrumbs + : [ + { + text: + app?.label ?? + i18n.translate('xpack.observabilityShared.breadcrumbs.observabilityLinkText', { + defaultMessage: 'Observability', + }), + href: appPath + '/overview', + }, + ...extraCrumbs, + ]; + + if (setBreadcrumbs) { + setBreadcrumbs(addClickHandlers(breadcrumbs, navigateToUrl)); + } + if (setTitle) { + setTitle(getTitleFromBreadCrumbs(breadcrumbs)); + } + }, [app?.label, appPath, extraCrumbs, navigateToUrl, serverless, setBreadcrumbs, setTitle]); +}; diff --git a/packages/kbn-typed-react-router-config/src/breadcrumbs/use_router_breadcrumb.ts b/packages/kbn-typed-react-router-config/src/breadcrumbs/use_router_breadcrumb.ts new file mode 100644 index 0000000000000..47003cbce5a26 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/breadcrumbs/use_router_breadcrumb.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useContext, useEffect, useRef } from 'react'; +import { castArray } from 'lodash'; +import { PathsOf, RouteMap, useCurrentRoute } from '../..'; +import { Breadcrumb, BreadcrumbsContext } from './context'; + +type UseBreadcrumbs = >( + callback: () => Breadcrumb | Array>, + fnDeps: unknown[] +) => void; + +export function useRouterBreadcrumb(callback: () => Breadcrumb | Breadcrumb[], fnDeps: any[]) { + const api = useContext(BreadcrumbsContext); + + if (!api) { + throw new Error('Missing Breadcrumb API in context'); + } + + const { match } = useCurrentRoute(); + + const matchedRoute = useRef(match?.route); + + useEffect(() => { + if (matchedRoute.current && matchedRoute.current !== match?.route) { + api.unset(matchedRoute.current); + } + + matchedRoute.current = match?.route; + + if (matchedRoute.current) { + api.set(matchedRoute.current, castArray(callback())); + } + + return () => { + if (matchedRoute.current) { + api.unset(matchedRoute.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [matchedRoute.current, match?.route, ...fnDeps]); +} + +export function createUseBreadcrumbs(): UseBreadcrumbs { + return useRouterBreadcrumb; +} diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 467fe096ffece..4321f4c0744a7 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -11,7 +11,7 @@ import { deepExactRt, mergeRt } from '@kbn/io-ts-utils'; import { isLeft } from 'fp-ts/lib/Either'; import { Location } from 'history'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { compact, findLastIndex, merge, orderBy } from 'lodash'; +import { compact, findLastIndex, mapValues, merge, orderBy } from 'lodash'; import qs from 'query-string'; import { MatchedRoute, @@ -139,7 +139,9 @@ export function createRouter(routes: TRoutes): Router< if (route?.params) { const decoded = deepExactRt(route.params).decode( merge({}, route.defaults ?? {}, { - path: matchedRoute.match.params, + path: mapValues(matchedRoute.match.params, (value) => { + return decodeURIComponent(value); + }), query: qs.parse(location.search, { decode: true }), }) ); @@ -179,7 +181,7 @@ export function createRouter(routes: TRoutes): Router< .split('/') .map((part) => { const match = part.match(/(?:{([a-zA-Z]+)})/); - return match ? paramsWithBuiltInDefaults.path[match[1]] : part; + return match ? encodeURIComponent(paramsWithBuiltInDefaults.path[match[1]]) : part; }) .join('/'); diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 3b4c36c42af53..dbab588619db7 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -106,6 +106,12 @@ export type TypeAsArgs = keyof TObject extends never ? [TObject] | [] : [TObject]; +export type TypeAsParams = keyof TObject extends never + ? {} + : RequiredKeys extends never + ? never + : { params: TObject }; + export type FlattenRoutesOf = Array< ValuesType<{ [key in keyof MapRoutes]: ValuesType[key]>; diff --git a/packages/kbn-typed-react-router-config/src/use_router.tsx b/packages/kbn-typed-react-router-config/src/use_router.tsx index 531bc5aea53b3..af92e33b8952a 100644 --- a/packages/kbn-typed-react-router-config/src/use_router.tsx +++ b/packages/kbn-typed-react-router-config/src/use_router.tsx @@ -20,7 +20,7 @@ export const RouterContextProvider = ({ children: React.ReactNode; }) => {children}; -export function useRouter(): Router { +export function useRouter(): Router { const router = useContext(RouterContext); if (!router) { diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index efda701736093..f9a133f48e8d5 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -14,7 +14,12 @@ ], "kbn_references": [ "@kbn/io-ts-utils", - "@kbn/shared-ux-router" + "@kbn/shared-ux-router", + "@kbn/core", + "@kbn/i18n", + "@kbn/kibana-react-plugin", + "@kbn/core-chrome-browser", + "@kbn/serverless" ], "exclude": [ "target/**/*", diff --git a/packages/kbn-unsaved-changes-prompt/src/unsaved_changes_prompt/unsaved_changes_prompt.test.tsx b/packages/kbn-unsaved-changes-prompt/src/unsaved_changes_prompt/unsaved_changes_prompt.test.tsx index 3b8c38caeef5d..5b155d657c48e 100644 --- a/packages/kbn-unsaved-changes-prompt/src/unsaved_changes_prompt/unsaved_changes_prompt.test.tsx +++ b/packages/kbn-unsaved-changes-prompt/src/unsaved_changes_prompt/unsaved_changes_prompt.test.tsx @@ -8,7 +8,8 @@ */ import { createMemoryHistory } from 'history'; -import { renderHook, act, cleanup } from '@testing-library/react-hooks'; + +import { renderHook, act, cleanup, waitFor } from '@testing-library/react'; import { coreMock } from '@kbn/core/public/mocks'; import { CoreScopedHistory } from '@kbn/core/public'; @@ -73,7 +74,7 @@ describe('useUnsavedChangesPrompt', () => { act(() => history.push('/test')); // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(navigateToUrl).toBeCalledWith('/mock/test', expect.anything()); expect(coreStart.overlays.openConfirm).toBeCalled(); diff --git a/packages/kbn-utility-types/src/dot.ts b/packages/kbn-utility-types/src/dot.ts index 40af0ce14c695..54b8cb5f5ac39 100644 --- a/packages/kbn-utility-types/src/dot.ts +++ b/packages/kbn-utility-types/src/dot.ts @@ -23,7 +23,9 @@ type DedotKey< export type DedotObject> = UnionToIntersection< Exclude< ValuesType<{ - [TKey in keyof TObject]: {} extends Pick + [TKey in keyof TObject as string]: string extends TKey + ? Record + : {} extends Pick ? DeepPartial>> : DedotKey; }>, diff --git a/packages/kbn-utility-types/src/tsd_tests/test_d/dot.ts b/packages/kbn-utility-types/src/tsd_tests/test_d/dot.ts index 855b84a3cbc10..1911db2e141be 100644 --- a/packages/kbn-utility-types/src/tsd_tests/test_d/dot.ts +++ b/packages/kbn-utility-types/src/tsd_tests/test_d/dot.ts @@ -7,9 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { expectAssignable, expectNotType, expectType } from 'tsd'; import { DedotObject, DotObject } from '../../dot'; +function isAssignable(t: T) {} + interface TestA { 'my.dotted.key': string; 'my.dotted.partial.key'?: string; @@ -60,20 +61,39 @@ const dedotted1 = {} as DedotObject; const dotted1 = {} as DotObject; -expectAssignable>({} as Dedotted); -expectAssignable>({} as Dotted); -expectAssignable({} as DedotObject); -expectAssignable({} as DotObject); - -expectType(dedotted1.ym?.dotted?.partial?.key?.toString()); -expectType(dotted1['my.undotted.key'].toString()); -expectNotType(dotted1['my.partial.key']); -expectType(dotted1['my.partial.key']?.toString()); -expectNotType<{ baz: string }>({} as DedotObject); -expectNotType<{ baz: string }>({} as DotObject); -expectNotType<{ my: { dotted: { key: string }; partial: { key: number } } }>( - {} as DedotObject -); +isAssignable>({} as Dedotted); + +isAssignable>({} as Dotted); +isAssignable({} as DedotObject); +isAssignable({} as DotObject); + +isAssignable(dedotted1.ym?.dotted?.partial?.key); + +isAssignable(dotted1['my.undotted.key'].toString()); +// @ts-expect-error +isAssignable(dotted1['my.partial.key']); + +isAssignable(dotted1['my.partial.key']?.toString()); + +// @ts-expect-error +isAssignable<{ baz: string }>({} as DedotObject); +// @ts-expect-error +isAssignable<{ baz: string }>({} as DotObject); + +// @ts-expect-error +isAssignable<{ my: { dotted: { key: string }; partial: { key: number } } }, DedotObject>(); + +type WithStringKey = { + [x: string]: string; +} & { + count: number; +}; + +type WithStringKeyDedotted = DedotObject; + +isAssignable({} as WithStringKey); + +isAssignable({} as WithStringKeyDedotted); interface ObjectWithArray { span: { @@ -88,7 +108,7 @@ interface ObjectWithArray { }; } -expectType>({ +isAssignable>({ 'span.links.span.id': [''], 'span.links.trace.id': [''], }); diff --git a/packages/kbn-visualization-utils/src/debounced_value.test.ts b/packages/kbn-visualization-utils/src/debounced_value.test.ts index 1f0d3af1d4f37..5ea0901067446 100644 --- a/packages/kbn-visualization-utils/src/debounced_value.test.ts +++ b/packages/kbn-visualization-utils/src/debounced_value.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useDebouncedValue } from './debounced_value'; describe('useDebouncedValue', () => { diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index 2b96c6d353eee..d3b9c44f6fd36 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -108,6 +108,7 @@ export { type PhaseEventType, type PublishesPhaseEvents, } from './interfaces/publishes_phase_events'; +export { apiPublishesRendered, type PublishesRendered } from './interfaces/publishes_rendered'; export { apiPublishesSavedObjectId, type PublishesSavedObjectId, diff --git a/packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts b/packages/presentation/presentation_publishing/interfaces/publishes_rendered.ts new file mode 100644 index 0000000000000..9acbd8c3f258f --- /dev/null +++ b/packages/presentation/presentation_publishing/interfaces/publishes_rendered.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PublishingSubject } from '../publishing_subject'; + +export interface PublishesRendered { + rendered$: PublishingSubject; +} + +export const apiPublishesRendered = ( + unknownApi: null | unknown +): unknownApi is PublishesRendered => { + return Boolean(unknownApi && (unknownApi as PublishesRendered)?.rendered$ !== undefined); +}; diff --git a/renovate.json b/renovate.json index 96af2c90b498d..9ec7446ebf5dc 100644 --- a/renovate.json +++ b/renovate.json @@ -307,6 +307,76 @@ "enabled": true, "minimumReleaseAge": "7 days" }, + { + "groupName": "formatjs dependencies", + "matchDepNames": [ + "@formatjs/icu-messageformat-parser", + "@formatjs/intl", + "@formatjs/intl-pluralrules", + "@formatjs/intl-relativetimeformat", + "@formatjs/intl-utils", + "@formatjs/ts-transformer" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Core", + "release_note:skip", + "backport:all-open" + ], + "enabled": true, + "minimumReleaseAge": "7 days" + }, + { + "groupName": "@elastic/kibana-core dependencies", + "matchDepNames": [ + "@elastic/request-crypto", + "ansi-regex", + "axios", + "cacheable-lookup", + "getos", + "has-ansi", + "joi-to-json", + "json5", + "load-json-file", + "mime-types", + "mock-fs", + "node-fetch", + "react-intl", + "reflect-metadata", + "type-detect", + "utility-types", + "@types/getos", + "@types/hapi__cookie", + "@types/hapi__h2o2", + "@types/hapi__hapi", + "@types/hapi__inert", + "@types/has-ansi", + "@types/json5", + "@types/mime", + "@types/mime-types", + "@types/mock-fs", + "@types/node-fetch", + "@types/type-detect" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Core", + "release_note:skip", + "backport:all-open" + ], + "enabled": true, + "minimumReleaseAge": "7 days" + }, { "groupName": "@elastic/charts", "matchDepNames": [ @@ -487,7 +557,7 @@ "labels": [ "release_note:skip", "Team:Core", - "backport:skip" + "backport:all-open" ], "enabled": true }, @@ -510,29 +580,12 @@ ], "enabled": true }, - { - "groupName": "ansi-regex", - "matchDepNames": [ - "ansi-regex" - ], - "reviewers": [ - "team:kibana-core" - ], - "matchBaseBranches": [ - "main" - ], - "labels": [ - "release_note:skip", - "Team:Core", - "backport:skip" - ], - "minimumReleaseAge": "7 days", - "enabled": true - }, { "groupName": "OpenAPI Spec", "matchDepNames": [ - "@redocly/cli" + "@apidevtools/swagger-parser", + "@redocly/cli", + "openapi-types" ], "reviewers": [ "team:kibana-core" @@ -543,7 +596,7 @@ "labels": [ "release_note:skip", "Team:Core", - "backport:skip" + "backport:all-open" ], "minimumReleaseAge": "7 days", "enabled": true diff --git a/scripts/dependency_usage.sh b/scripts/dependency_usage.sh new file mode 100755 index 0000000000000..ccee69e62d348 --- /dev/null +++ b/scripts/dependency_usage.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Need to tun the script with ts-node/esm since dependency-cruiser is only available as an ESM module. +# We specify the correct tsconfig.json file to ensure compatibility, as our current setup doesn’t fully support ESM modules. +# Should be resolved after https://github.com/elastic/kibana/issues/198790 is done. +NODE_NO_WARNINGS=1 TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=packages/kbn-dependency-usage/tsconfig.json \ +node --loader ts-node/esm packages/kbn-dependency-usage/src/cli.ts "$@" diff --git a/scripts/scout_start_servers.js b/scripts/scout_start_servers.js new file mode 100644 index 0000000000000..b93ec0e456454 --- /dev/null +++ b/scripts/scout_start_servers.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +require('../src/setup_node_env'); +require('@kbn/scout').startServersCli(); diff --git a/scripts/scout_test.js b/scripts/scout_test.js new file mode 100644 index 0000000000000..8b14ebd33da19 --- /dev/null +++ b/scripts/scout_test.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +require('../src/setup_node_env'); +require('@kbn/scout').runTestsCli(); diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index f16c956107c7b..fe08e3737800e 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -108,7 +108,7 @@ describe('checking migration metadata changes on all registered SO types', () => "fleet-agent-policies": "f57d3b70e4175a19a18f18ee72a379ceec82e1fc", "fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417", "fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f", - "fleet-package-policies": "2f4d524adb49a5281d3af0b66bb3003ba0ff2e44", + "fleet-package-policies": "0206c20f27286787b91814a2e7872f06dc1e8e47", "fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e", "fleet-proxy": "6cb688f0d2dd856400c1dbc998b28704ff70363d", "fleet-setup-lock": "0dc784792c79b5af5a6e6b5dcac06b0dbaa90bde", @@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "5e95e539826a40ad08fd0c1d161da0a4d86ffc6d", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "55988d5f778bbe0e76caa7e6468707a0a056bdd8", - "ingest-package-policies": "53a94064674835fdb35e5186233bcd7052eabd22", + "ingest-package-policies": "60d43f475f91417d14d9df05476acf2e63e99435", "ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts index 8213c880c0fa4..e8a523b5de50e 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/incompatible_cluster_routing_allocation.test.ts @@ -92,7 +92,8 @@ async function updateRoutingAllocations( }); } -describe('incompatible_cluster_routing_allocation', () => { +// Failing ES promotion: https://github.com/elastic/kibana/issues/158318 +describe.skip('incompatible_cluster_routing_allocation', () => { let client: ElasticsearchClient; let root: Root; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts index 6898962077b9c..5be7b4671ef72 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts @@ -96,7 +96,8 @@ function createRoot({ logFileName, hosts }: RootConfig) { }); } -describe('migration v2', () => { +// Failing ES promotion: https://github.com/elastic/kibana/issues/167676 +describe.skip('migration v2', () => { let esServer: TestElasticsearchUtils; let root: Root; const migratedIndexAlias = `.kibana_${pkg.version}`; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts index 9f970ed234d71..6ebc7ee5a66f6 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts @@ -19,7 +19,9 @@ import { getFips } from 'crypto'; const logFilePath = join(__dirname, 'read_batch_size.log'); -describe('migration v2 - read batch size', () => { +// Failing ES promotion: https://github.com/elastic/kibana/issues/163254 +// Failing ES promotion: https://github.com/elastic/kibana/issues/163255 +describe.skip('migration v2 - read batch size', () => { let esServer: TestElasticsearchUtils; let root: Root; let logs: string; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 186360c03e805..be9fe9f12512d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -51,7 +51,7 @@ export async function runDockerGenerator( */ if (flags.baseImage === 'wolfi') baseImageName = - 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:32099b99697d9da842c1ccacdbef1beee05a68cddb817e858d7656df45ed4c93'; + 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:32f06b169bb4b0f257fbb10e8c8379f06d3ee1355c89b3327cb623781a29590e'; let imageFlavor = ''; if (flags.baseImage === 'ubi') imageFlavor += `-ubi`; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml index d1d76367b4ec3..7d063b49151bd 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/hardening_manifest.yaml @@ -11,7 +11,7 @@ tags: # Build args passed to Dockerfile ARGs args: BASE_IMAGE: 'redhat/ubi/ubi9' - BASE_TAG: "9.4" + BASE_TAG: "9.5" # Docker image labels labels: org.opencontainers.image.title: 'kibana' @@ -85,4 +85,4 @@ maintainers: - email: 'klepal_alexander@bah.com' name: 'Alexander Klepal' username: 'alexander.klepal' - cht_member: true \ No newline at end of file + cht_member: true diff --git a/src/dev/eslint/run_eslint_with_types.ts b/src/dev/eslint/run_eslint_with_types.ts index 8a70945688ddf..0a872a248dc5a 100644 --- a/src/dev/eslint/run_eslint_with_types.ts +++ b/src/dev/eslint/run_eslint_with_types.ts @@ -28,7 +28,7 @@ export function runEslintWithTypes() { async ({ log, flags }) => { const ignoreFilePath = Path.resolve(REPO_ROOT, '.eslintignore'); const configTemplate = Fs.readFileSync( - Path.resolve(__dirname, 'types.eslint.config.template.js'), + Path.resolve(__dirname, 'types.eslint.config.template.cjs'), 'utf8' ); @@ -65,7 +65,7 @@ export function runEslintWithTypes() { const failures = await Rx.lastValueFrom( Rx.from(projects).pipe( mergeMap(async (project) => { - const configFilePath = Path.resolve(project.directory, 'types.eslint.config.js'); + const configFilePath = Path.resolve(project.directory, 'types.eslint.config.cjs'); Fs.writeFileSync( configFilePath, diff --git a/src/dev/eslint/types.eslint.config.template.js b/src/dev/eslint/types.eslint.config.template.cjs similarity index 100% rename from src/dev/eslint/types.eslint.config.template.js rename to src/dev/eslint/types.eslint.config.template.cjs diff --git a/src/dev/run_check_file_casing.ts b/src/dev/run_check_file_casing.ts index 9ac610df14bd7..6b77e326abef8 100644 --- a/src/dev/run_check_file_casing.ts +++ b/src/dev/run_check_file_casing.ts @@ -26,6 +26,12 @@ run(async ({ log }) => { // so it's still super slow. This prevents loading the files // and still relies on gitignore to final ignores '**/node_modules', + // temporarily allow non-snake case for module names + // during relocation packages and plugins + // in the context of Sustainable Kibana Architecture + 'src/platform/**', + 'x-pack/platform/**', + 'x-pack/solutions/**', ], }); diff --git a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts index adc2d291483c5..1679ad16dae52 100644 --- a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts +++ b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts @@ -8,7 +8,7 @@ */ import { TestScheduler } from 'rxjs/testing'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { Chart, PointerEvent } from '@elastic/charts'; import type { Datatable } from '@kbn/expressions-plugin/public'; import type { RefObject } from 'react'; diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx index ef77405edf27e..da89ec475d577 100644 --- a/src/plugins/charts/public/services/theme/theme.test.tsx +++ b/src/plugins/charts/public/services/theme/theme.test.tsx @@ -10,8 +10,7 @@ import React from 'react'; import { from } from 'rxjs'; import { take } from 'rxjs'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { render, act as renderAct } from '@testing-library/react'; +import { render, act as renderAct, renderHook, act } from '@testing-library/react'; import { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; diff --git a/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.test.ts b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.test.ts index 0605a0c903ee0..4dab86712707d 100644 --- a/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.test.ts +++ b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useSetInitialValue } from './use_set_initial_value'; import { IToasts } from '@kbn/core-notifications-browser'; import { decompressFromEncodedURIComponent } from 'lz-string'; diff --git a/src/plugins/console/public/application/containers/editor/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco_editor.tsx index bc174b772bb1c..3e4728da27ae3 100644 --- a/src/plugins/console/public/application/containers/editor/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco_editor.tsx @@ -211,9 +211,11 @@ export const MonacoEditor = ({ localStorageValue, value, setValue }: EditorProps fontSize: settings.fontSize, wordWrap: settings.wrapMode === true ? 'on' : 'off', theme: CONSOLE_THEME_ID, - // Make the quick-fix window be fixed to the window rather than clipped by - // the parent content set with overflow: hidden/auto - fixedOverflowWidgets: true, + // Force the hover views to always render below the cursor to avoid clipping + // when the cursor is near the top of the editor. + hover: { + above: false, + }, }} suggestionProvider={suggestionProvider} enableFindAction={true} diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 6c8c8f11d6a13..e434fe19d7b26 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -213,6 +213,10 @@ export const useDashboardListingTable = ({ size: listingLimit, hasReference: references, hasNoReference: referencesToExclude, + options: { + // include only tags references in the response to save bandwidth + includeReferences: ['tag'], + }, }) .then(({ total, hits }) => { const searchEndTime = window.performance.now(); diff --git a/src/plugins/dashboard/server/content_management/dashboard_storage.ts b/src/plugins/dashboard/server/content_management/dashboard_storage.ts index e65002802989f..d113d509f5e89 100644 --- a/src/plugins/dashboard/server/content_management/dashboard_storage.ts +++ b/src/plugins/dashboard/server/content_management/dashboard_storage.ts @@ -341,7 +341,10 @@ export class DashboardStorage { const soResponse = await soClient.find(soQuery); const hits = soResponse.saved_objects .map((so) => { - const { item } = savedObjectToItem(so, false, soQuery.fields); + const { item } = savedObjectToItem(so, false, { + allowedAttributes: soQuery.fields, + allowedReferences: optionsToLatest?.includeReferences, + }); return item; }) // Ignore any saved objects that failed to convert to items. diff --git a/src/plugins/dashboard/server/content_management/v3/cm_services.ts b/src/plugins/dashboard/server/content_management/v3/cm_services.ts index e086d1cc1460a..d2a53309704c6 100644 --- a/src/plugins/dashboard/server/content_management/v3/cm_services.ts +++ b/src/plugins/dashboard/server/content_management/v3/cm_services.ts @@ -437,6 +437,7 @@ export const dashboardSearchOptionsSchema = schema.maybe( { onlyTitle: schema.maybe(schema.boolean()), fields: schema.maybe(schema.arrayOf(schema.string())), + includeReferences: schema.maybe(schema.arrayOf(schema.oneOf([schema.literal('tag')]))), kuery: schema.maybe(schema.string()), cursor: schema.maybe(schema.number()), limit: schema.maybe(schema.number()), diff --git a/src/plugins/dashboard/server/content_management/v3/transform_utils.test.ts b/src/plugins/dashboard/server/content_management/v3/transform_utils.test.ts index 627f1c4211033..1cb10c8a10def 100644 --- a/src/plugins/dashboard/server/content_management/v3/transform_utils.test.ts +++ b/src/plugins/dashboard/server/content_management/v3/transform_utils.test.ts @@ -432,7 +432,9 @@ describe('savedObjectToItem', () => { }, }; - const { item, error } = savedObjectToItem(input, true, ['title', 'description']); + const { item, error } = savedObjectToItem(input, true, { + allowedAttributes: ['title', 'description'], + }); expect(error).toBeNull(); expect(item).toEqual({ ...commonSavedObject, @@ -457,6 +459,60 @@ describe('savedObjectToItem', () => { expect(item).toBeNull(); expect(error).not.toBe(null); }); + + it('should include only requested references', () => { + const input = { + ...commonSavedObject, + references: [ + { + type: 'tag', + id: 'tag1', + name: 'tag-ref-tag1', + }, + { + type: 'index-pattern', + id: 'index-pattern1', + name: 'index-pattern-ref-index-pattern1', + }, + ], + attributes: { + title: 'title', + description: 'my description', + timeRestore: false, + }, + }; + + { + const { item } = savedObjectToItem(input, true, { + allowedAttributes: ['title', 'description'], + }); + expect(item?.references).toEqual(input.references); + } + + { + const { item } = savedObjectToItem(input, true, { + allowedAttributes: ['title', 'description'], + allowedReferences: ['tag'], + }); + expect(item?.references).toEqual([input.references[0]]); + } + + { + const { item } = savedObjectToItem(input, true, { + allowedAttributes: ['title', 'description'], + allowedReferences: [], + }); + expect(item?.references).toEqual([]); + } + + { + const { item } = savedObjectToItem({ ...input, references: undefined }, true, { + allowedAttributes: ['title', 'description'], + allowedReferences: [], + }); + expect(item?.references).toBeUndefined(); + } + }); }); describe('getResultV3ToV2', () => { diff --git a/src/plugins/dashboard/server/content_management/v3/transform_utils.ts b/src/plugins/dashboard/server/content_management/v3/transform_utils.ts index 843dd59f849f3..18c9085df4ec0 100644 --- a/src/plugins/dashboard/server/content_management/v3/transform_utils.ts +++ b/src/plugins/dashboard/server/content_management/v3/transform_utils.ts @@ -304,24 +304,35 @@ type PartialSavedObject = Omit>, 'references'> & { references: SavedObjectReference[] | undefined; }; +export interface SavedObjectToItemOptions { + /** + * attributes to include in the output item + */ + allowedAttributes?: string[]; + /** + * references to include in the output item + */ + allowedReferences?: string[]; +} + export function savedObjectToItem( savedObject: SavedObject, partial: false, - allowedAttributes?: string[] + opts?: SavedObjectToItemOptions ): SavedObjectToItemReturn; export function savedObjectToItem( savedObject: PartialSavedObject, partial: true, - allowedAttributes?: string[] + opts?: SavedObjectToItemOptions ): SavedObjectToItemReturn; export function savedObjectToItem( savedObject: | SavedObject | PartialSavedObject, - partial: boolean, - allowedAttributes?: string[] + partial: boolean /* partial arg is used to enforce the correct savedObject type */, + { allowedAttributes, allowedReferences }: SavedObjectToItemOptions = {} ): SavedObjectToItemReturn { const { id, @@ -342,6 +353,12 @@ export function savedObjectToItem( const attributesOut = allowedAttributes ? pick(dashboardAttributesOut(attributes), allowedAttributes) : dashboardAttributesOut(attributes); + + // if includeReferences is provided, only include references of those types + const referencesOut = allowedReferences + ? references?.filter((reference) => allowedReferences.includes(reference.type)) + : references; + return { item: { id, @@ -353,7 +370,7 @@ export function savedObjectToItem( attributes: attributesOut, error, namespaces, - references, + references: referencesOut, version, managed, }, diff --git a/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.test.ts b/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.test.ts new file mode 100644 index 0000000000000..700c90f08ce5b --- /dev/null +++ b/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject, skip } from 'rxjs'; +import { PhaseTracker } from './phase_tracker'; + +describe('PhaseTracker', () => { + describe('api does not implement PublishesDataLoading or PublishesRendered', () => { + test(`should emit 'rendered' event`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('rendered'); + done(); + }); + phaseTracker.trackPhaseEvents('1', {}); + }); + }); + + describe('api implements PublishesDataLoading', () => { + test(`should emit 'loading' event when dataLoading is true`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('loading'); + done(); + }); + phaseTracker.trackPhaseEvents('1', { dataLoading: new BehaviorSubject(true) }); + }); + + test(`should emit 'rendered' event when dataLoading is false`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('rendered'); + done(); + }); + phaseTracker.trackPhaseEvents('1', { dataLoading: new BehaviorSubject(false) }); + }); + }); + + describe('api implements PublishesDataLoading and PublishesRendered', () => { + test(`should emit 'loading' event when dataLoading is true`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('loading'); + done(); + }); + phaseTracker.trackPhaseEvents('1', { + dataLoading: new BehaviorSubject(true), + rendered$: new BehaviorSubject(false), + }); + }); + + test(`should emit 'loading' event when dataLoading is false but rendered is false`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('loading'); + done(); + }); + phaseTracker.trackPhaseEvents('1', { + dataLoading: new BehaviorSubject(false), + rendered$: new BehaviorSubject(false), + }); + }); + + test(`should emit 'rendered' event only when rendered is true`, (done) => { + const phaseTracker = new PhaseTracker(); + phaseTracker + .getPhase$() + .pipe(skip(1)) + .subscribe((phaseEvent) => { + expect(phaseEvent?.status).toBe('rendered'); + done(); + }); + phaseTracker.trackPhaseEvents('1', { + dataLoading: new BehaviorSubject(false), + rendered$: new BehaviorSubject(true), + }); + }); + }); +}); diff --git a/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.ts b/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.ts new file mode 100644 index 0000000000000..037599ab646cc --- /dev/null +++ b/src/plugins/embeddable/public/react_embeddable_system/phase_tracker.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + PhaseEvent, + apiPublishesDataLoading, + apiPublishesRendered, +} from '@kbn/presentation-publishing'; +import { BehaviorSubject, Subscription, combineLatest } from 'rxjs'; + +export class PhaseTracker { + private firstLoadCompleteTime: number | undefined; + private embeddableStartTime = performance.now(); + private subscriptions = new Subscription(); + private phase$ = new BehaviorSubject(undefined); + + getPhase$() { + return this.phase$; + } + + public trackPhaseEvents(uuid: string, api: unknown) { + const dataLoading$ = apiPublishesDataLoading(api) + ? api.dataLoading + : new BehaviorSubject(false); + const rendered$ = apiPublishesRendered(api) ? api.rendered$ : new BehaviorSubject(true); + + this.subscriptions.add( + combineLatest([dataLoading$, rendered$]).subscribe(([dataLoading, rendered]) => { + if (!this.firstLoadCompleteTime) { + this.firstLoadCompleteTime = performance.now(); + } + const duration = this.firstLoadCompleteTime - this.embeddableStartTime; + const status = dataLoading || !rendered ? 'loading' : 'rendered'; + this.phase$.next({ id: uuid, status, timeToEvent: duration }); + }) + ); + } + + public cleanup() { + this.subscriptions.unsubscribe(); + } +} diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index c3dc06e198cd8..edf52244c2d4d 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -16,12 +16,7 @@ import { SerializedPanelState, } from '@kbn/presentation-containers'; import { PresentationPanel, PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; -import { - apiPublishesDataLoading, - ComparatorDefinition, - PhaseEvent, - StateComparators, -} from '@kbn/presentation-publishing'; +import { ComparatorDefinition, StateComparators } from '@kbn/presentation-publishing'; import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import { BehaviorSubject, combineLatest, debounceTime, skip, Subscription, switchMap } from 'rxjs'; import { v4 as generateId } from 'uuid'; @@ -31,6 +26,7 @@ import { DefaultEmbeddableApi, SetReactEmbeddableApiRegistration, } from './types'; +import { PhaseTracker } from './phase_tracker'; const ON_STATE_CHANGE_DEBOUNCE = 100; @@ -69,6 +65,7 @@ export const ReactEmbeddableRenderer = < | 'hideLoader' | 'hideHeader' | 'hideInspector' + | 'getActions' >; hidePanelChrome?: boolean; /** @@ -78,25 +75,12 @@ export const ReactEmbeddableRenderer = < onAnyStateChange?: (state: SerializedPanelState) => void; }) => { const cleanupFunction = useRef<(() => void) | null>(null); - const firstLoadCompleteTime = useRef(null); + const phaseTracker = useRef(new PhaseTracker()); const componentPromise = useMemo( () => { const uuid = maybeId ?? generateId(); - /** - * Phase tracking instrumentation for telemetry - */ - const phase$ = new BehaviorSubject(undefined); - const embeddableStartTime = performance.now(); - const reportPhaseChange = (loading: boolean) => { - if (firstLoadCompleteTime.current === null) { - firstLoadCompleteTime.current = performance.now(); - } - const duration = firstLoadCompleteTime.current - embeddableStartTime; - phase$.next({ id: uuid, status: loading ? 'loading' : 'rendered', timeToEvent: duration }); - }; - /** * Build the embeddable promise */ @@ -126,7 +110,7 @@ export const ReactEmbeddableRenderer = < return { ...apiRegistration, uuid, - phase$, + phase$: phaseTracker.current.getPhase$(), parentApi, hasLockedHoverActions$, lockHoverActions: (lock: boolean) => { @@ -186,6 +170,7 @@ export const ReactEmbeddableRenderer = < cleanupFunction.current = () => { subscriptions.unsubscribe(); + phaseTracker.current.cleanup(); unsavedChanges.cleanup(); }; return fullApi as Api & HasSnapshottableState; @@ -200,13 +185,8 @@ export const ReactEmbeddableRenderer = < lastSavedRuntimeState ); - if (apiPublishesDataLoading(api)) { - subscriptions.add( - api.dataLoading.subscribe((loading) => reportPhaseChange(Boolean(loading))) - ); - } else { - reportPhaseChange(false); - } + phaseTracker.current.trackPhaseEvents(uuid, api); + return { api, Component }; }; diff --git a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx index 41f1fd16148f4..cac179d45f946 100644 --- a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx +++ b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx @@ -20,6 +20,7 @@ import { EmbeddableComponent, FieldBasedIndexPatternColumn, TypedLensByValueInput, + LensByValueInput, } from '@kbn/lens-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/common'; import { render, screen, waitFor } from '@testing-library/react'; @@ -27,7 +28,6 @@ import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import { I18nProvider } from '@kbn/i18n-react'; import { GroupPreview } from './group_preview'; -import { LensByValueInput } from '@kbn/lens-plugin/public/embeddable'; import { DATA_LAYER_ID, DATE_HISTOGRAM_COLUMN_ID, getCurrentTimeField } from './lens_attributes'; import { EuiSuperDatePickerTestHarness } from '@kbn/test-eui-helpers'; diff --git a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.tsx b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.tsx index 3f1c47f3a72b7..5f03d67092331 100644 --- a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.tsx +++ b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.tsx @@ -198,28 +198,25 @@ export const GroupPreview = ({ justifyContent="center" > -
div { height: 400px; width: 100%; } `} - > - - setChartTimeRange({ - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }) - } - searchSessionId={searchSessionId} - /> -
+ data-test-subj="chart" + id="annotation-library-preview" + timeRange={chartTimeRange} + attributes={lensAttributes} + onBrushEnd={({ range }) => + setChartTimeRange({ + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }) + } + searchSessionId={searchSessionId} + />
) : ( diff --git a/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.test.ts b/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.test.ts index ebbe6e4129019..8f32edaac50ab 100644 --- a/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.test.ts +++ b/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useDebouncedValue } from './use_debounced_value'; describe('useDebouncedValue', () => { diff --git a/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.test.ts b/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.test.ts index ac9ff31730f0a..9dc3ab684ccb9 100644 --- a/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.test.ts +++ b/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.test.ts @@ -8,7 +8,7 @@ */ import type { RefObject } from 'react'; -import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, act, RenderHookResult } from '@testing-library/react'; import { Subject } from 'rxjs'; import type { IInterpreterRenderHandlers } from '../../common'; import { ExpressionRendererParams, useExpressionRenderer } from './use_expression_renderer'; @@ -23,7 +23,7 @@ describe('useExpressionRenderer', () => { loading$: Subject; render$: Subject; }; - let hook: RenderHookResult>; + let hook: RenderHookResult, ExpressionRendererParams>; beforeEach(() => { nodeRef = { current: document.createElement('div') }; diff --git a/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.ts b/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.ts index 2d5f5d6ddd493..06d588263869f 100644 --- a/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.ts +++ b/src/plugins/expressions/public/react_expression_renderer/use_expression_renderer.ts @@ -26,7 +26,7 @@ export interface ExpressionRendererParams extends IExpressionLoaderParams { debounce?: number; expression: string | ExpressionAstExpression; hasCustomErrorRenderer?: boolean; - onData$?( + onData$?( data: TData, adapters?: TInspectorAdapters, partial?: boolean diff --git a/src/plugins/expressions/public/react_expression_renderer/use_shallow_memo.test.ts b/src/plugins/expressions/public/react_expression_renderer/use_shallow_memo.test.ts index acfa528c932b7..9e073c86dbeea 100644 --- a/src/plugins/expressions/public/react_expression_renderer/use_shallow_memo.test.ts +++ b/src/plugins/expressions/public/react_expression_renderer/use_shallow_memo.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useShallowMemo } from './use_shallow_memo'; describe('useShallowMemo', () => { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 88d60b1a86b2e..ad2dce80fb650 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -143,6 +143,7 @@ export const applicationUsageSchema = { enterpriseSearchSemanticSearch: commonSchema, enterpriseSearchVectorSearch: commonSchema, enterpriseSearchElasticsearch: commonSchema, + entity_manager: commonSchema, appSearch: commonSchema, workplaceSearch: commonSchema, searchExperiences: commonSchema, @@ -182,6 +183,7 @@ export const applicationUsageSchema = { */ siem: commonSchema, space_selector: commonSchema, + streams: commonSchema, uptime: commonSchema, synthetics: commonSchema, ux: commonSchema, diff --git a/src/plugins/navigation/public/mocks.ts b/src/plugins/navigation/public/mocks.tsx similarity index 54% rename from src/plugins/navigation/public/mocks.ts rename to src/plugins/navigation/public/mocks.tsx index b9977daf56223..5f9f1476b4648 100644 --- a/src/plugins/navigation/public/mocks.ts +++ b/src/plugins/navigation/public/mocks.tsx @@ -6,13 +6,24 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - +import React from 'react'; import { of } from 'rxjs'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { Plugin } from '.'; +import { createTopNav } from './top_nav_menu'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; +// mock mountPointPortal +jest.mock('@kbn/react-kibana-mount', () => { + const original = jest.requireActual('@kbn/react-kibana-mount'); + return { + ...original, + MountPointPortal: jest.fn(({ children }) => children), + }; +}); + const createSetupContract = (): jest.Mocked => { const setupContract = { registerMenuItem: jest.fn(), @@ -21,12 +32,21 @@ const createSetupContract = (): jest.Mocked => { return setupContract; }; +export const unifiedSearchMock = { + ui: { + SearchBar: () =>
, + AggregateQuerySearchBar: () =>
, + }, +} as unknown as UnifiedSearchPublicPluginStart; + const createStartContract = (): jest.Mocked => { const startContract = { ui: { - TopNavMenu: jest.fn(), - createTopNavWithCustomContext: jest.fn().mockImplementation(() => jest.fn()), - AggregateQueryTopNavMenu: jest.fn(), + TopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])), + AggregateQueryTopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])), + createTopNavWithCustomContext: jest + .fn() + .mockImplementation(createTopNav(unifiedSearchMock, [])), }, addSolutionNavigation: jest.fn(), isSolutionNavEnabled$: of(false), diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index dff09fa0bac38..5ad6e2bbe5dd4 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -14,16 +14,9 @@ import { MountPoint } from '@kbn/core/public'; import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { EuiToolTipProps } from '@elastic/eui'; import type { TopNavMenuBadgeProps } from './top_nav_menu_badges'; - -const unifiedSearch = { - ui: { - SearchBar: () =>
, - AggregateQuerySearchBar: () =>
, - }, -} as unknown as UnifiedSearchPublicPluginStart; +import { unifiedSearchMock } from '../mocks'; describe('TopNavMenu', () => { const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper'; @@ -97,7 +90,7 @@ describe('TopNavMenu', () => { it('Should render search bar', () => { const component = mountWithIntl( - + ); expect(component.find(WRAPPER_SELECTOR).length).toBe(1); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); @@ -110,7 +103,7 @@ describe('TopNavMenu', () => { appName={'test'} config={menuItems} showSearchBar={true} - unifiedSearch={unifiedSearch} + unifiedSearch={unifiedSearchMock} /> ); expect(component.find(WRAPPER_SELECTOR).length).toBe(1); @@ -124,7 +117,7 @@ describe('TopNavMenu', () => { appName={'test'} config={menuItems} showSearchBar={true} - unifiedSearch={unifiedSearch} + unifiedSearch={unifiedSearchMock} className={'myCoolClass'} /> ); @@ -172,7 +165,7 @@ describe('TopNavMenu', () => { appName={'test'} config={menuItems} showSearchBar={true} - unifiedSearch={unifiedSearch} + unifiedSearch={unifiedSearchMock} setMenuMountPoint={setMountPoint} /> ); @@ -195,7 +188,7 @@ describe('TopNavMenu', () => { appName={'test'} badges={badges} showSearchBar={true} - unifiedSearch={unifiedSearch} + unifiedSearch={unifiedSearchMock} setMenuMountPoint={setMountPoint} /> ); diff --git a/src/plugins/share/public/components/context/index.test.tsx b/src/plugins/share/public/components/context/index.test.tsx new file mode 100644 index 0000000000000..162518e505b1d --- /dev/null +++ b/src/plugins/share/public/components/context/index.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useShareTabsContext } from '.'; + +describe('share menu context', () => { + describe('useShareTabsContext', () => { + it('throws an error if used outside of ShareMenuProvider tree', () => { + const { result } = renderHook(() => useShareTabsContext()); + + expect(result.error?.message).toEqual( + expect.stringContaining( + 'Failed to call `useShareTabsContext` because the context from ShareMenuProvider is missing.' + ) + ); + }); + }); +}); diff --git a/src/plugins/share/public/components/context/index.tsx b/src/plugins/share/public/components/context/index.tsx index b75df40aaa41a..13d6138e42a60 100644 --- a/src/plugins/share/public/components/context/index.tsx +++ b/src/plugins/share/public/components/context/index.tsx @@ -9,7 +9,7 @@ import { ThemeServiceSetup } from '@kbn/core-theme-browser'; import { I18nStart } from '@kbn/core/public'; -import { createContext, useContext } from 'react'; +import React, { type PropsWithChildren, createContext, useContext } from 'react'; import { AnonymousAccessServiceContract } from '../../../common'; import type { @@ -35,6 +35,23 @@ export interface IShareContext extends ShareContext { anchorElement?: HTMLElement; } -export const ShareTabsContext = createContext(null); +const ShareTabsContext = createContext(null); -export const useShareTabsContext = () => useContext(ShareTabsContext); +export const ShareMenuProvider = ({ + shareContext, + children, +}: PropsWithChildren<{ shareContext: IShareContext }>) => { + return {children}; +}; + +export const useShareTabsContext = () => { + const context = useContext(ShareTabsContext); + + if (!context) { + throw new Error( + 'Failed to call `useShareTabsContext` because the context from ShareMenuProvider is missing. Ensure the component or React root is wrapped with ShareMenuProvider' + ); + } + + return context; +}; diff --git a/src/plugins/share/public/components/share_tabs.test.tsx b/src/plugins/share/public/components/share_tabs.test.tsx index b4ad92fce84f9..6b04a28304fdd 100644 --- a/src/plugins/share/public/components/share_tabs.test.tsx +++ b/src/plugins/share/public/components/share_tabs.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ShareMenuTabs } from './share_tabs'; -import { ShareTabsContext } from './context'; +import { ShareMenuProvider } from './context'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { KibanaLocation, LocatorGetUrlParams, UrlService } from '../../common/url_service'; import { @@ -77,9 +77,9 @@ describe('Share modal tabs', () => { }, ]; const wrapper = mountWithIntl( - + - + ); expect(wrapper.find('[data-test-subj="export"]').exists()).toBeTruthy(); }); @@ -92,11 +92,13 @@ describe('Share modal tabs', () => { generateExportUrl: mockGenerateExportUrl, }, ]; + const wrapper = mountWithIntl( - + - + ); + expect(wrapper.find('[data-test-subj="export"]').exists()).toBeFalsy(); }); @@ -116,9 +118,9 @@ describe('Share modal tabs', () => { }, ]; const wrapper = mountWithIntl( - + - + ); expect(wrapper.find('[data-test-subj="export"]').exists()).toBeTruthy(); }); diff --git a/src/plugins/share/public/components/share_tabs.tsx b/src/plugins/share/public/components/share_tabs.tsx index 0ed540a612009..94c4ab8655dca 100644 --- a/src/plugins/share/public/components/share_tabs.tsx +++ b/src/plugins/share/public/components/share_tabs.tsx @@ -8,16 +8,16 @@ */ import React, { type FC } from 'react'; -import { TabbedModal } from '@kbn/shared-ux-tabbed-modal'; +import { TabbedModal, type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal'; -import { ShareTabsContext, useShareTabsContext, type IShareContext } from './context'; +import { ShareMenuProvider, useShareTabsContext, type IShareContext } from './context'; import { linkTab, embedTab, exportTab } from './tabs'; export const ShareMenu: FC<{ shareContext: IShareContext }> = ({ shareContext }) => { return ( - + - + ); }; @@ -25,15 +25,9 @@ export const ShareMenu: FC<{ shareContext: IShareContext }> = ({ shareContext }) export const ShareMenuTabs = () => { const shareContext = useShareTabsContext(); - if (!shareContext) { - return null; - } - const { allowEmbed, objectTypeMeta, onClose, shareMenuItems, anchorElement } = shareContext; - const tabs = []; - - tabs.push(linkTab); + const tabs: Array> = [linkTab]; const enabledItems = shareMenuItems.filter(({ shareMenuItem }) => !shareMenuItem?.disabled); diff --git a/src/plugins/share/public/components/tabs/link/link_content.test.tsx b/src/plugins/share/public/components/tabs/link/link_content.test.tsx new file mode 100644 index 0000000000000..77fac4afc8ce0 --- /dev/null +++ b/src/plugins/share/public/components/tabs/link/link_content.test.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { type ComponentProps } from 'react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from '@testing-library/react'; + +import { urlServiceTestSetup } from '../../../../common/url_service/__tests__/setup'; +import { MockLocatorDefinition } from '../../../../common/url_service/mocks'; +import { BrowserShortUrlClientFactory } from '../../../url_service/short_urls/short_url_client_factory'; +import { + BrowserShortUrlClientHttp, + BrowserShortUrlClient, +} from '../../../url_service/short_urls/short_url_client'; +import { BrowserUrlService } from '../../../types'; +import { LinkContent } from './link_content'; + +const renderComponent = (props: ComponentProps) => { + render( + + + + ); +}; + +describe('LinkContent', () => { + const shareableUrl = 'http://localhost:5601/app/dashboards#/view/123'; + + const http: BrowserShortUrlClientHttp = { + basePath: { + get: () => '/xyz', + }, + fetch: jest.fn(async () => { + return {} as any; + }), + }; + + let urlService: BrowserUrlService; + + // @ts-expect-error there is a type because we override the shortUrls implementation + // eslint-disable-next-line prefer-const + ({ service: urlService } = urlServiceTestSetup({ + shortUrls: ({ locators }) => + new BrowserShortUrlClientFactory({ + http, + locators, + }), + })); + + beforeAll(() => { + Object.defineProperty(document, 'execCommand', { + value: jest.fn(() => true), + }); + }); + + it('uses the delegatedShareUrlHandler to generate the shareable URL when it is provided', async () => { + const user = userEvent.setup(); + const objectType = 'dashboard'; + const objectId = '123'; + const isDirty = false; + + const delegatedShareUrlHandler = jest.fn(); + + renderComponent({ + objectType, + objectId, + isDirty, + shareableUrl, + urlService, + allowShortUrl: true, + delegatedShareUrlHandler, + }); + + await user.click(screen.getByTestId('copyShareUrlButton')); + + expect(delegatedShareUrlHandler).toHaveBeenCalled(); + }); + + it('returns the shareable URL when the delegatedShareUrlHandler is not provided and shortURLs are not allowed', async () => { + const user = userEvent.setup(); + const objectType = 'dashboard'; + const objectId = '123'; + const isDirty = false; + + renderComponent({ + objectType, + objectId, + isDirty, + shareableUrl, + urlService, + allowShortUrl: false, + }); + + const copyButton = screen.getByTestId('copyShareUrlButton'); + + await user.click(copyButton); + + waitFor(() => { + expect(copyButton.getAttribute('data-share-url')).toBe(shareableUrl); + }); + }); + + it('invokes the createWithLocator method on the shortURL service if a locator is present when the copy button is clicked', async () => { + const user = userEvent.setup(); + const objectType = 'dashboard'; + const objectId = '123'; + const isDirty = false; + const shareableUrlLocatorParams = { + locator: new MockLocatorDefinition('TEST_LOCATOR'), + params: {}, + }; + + const shortURL = 'http://localhost:5601/xyz/r/s/yellow-orange-tomato'; + + const createWithLocatorSpy = jest.spyOn(BrowserShortUrlClient.prototype, 'createWithLocator'); + + createWithLocatorSpy.mockResolvedValue({ + // @ts-expect-error we only return locator property, as that's all we need for this test + locator: { + getUrl: jest.fn(() => Promise.resolve(shortURL)), + }, + }); + + renderComponent({ + objectType, + objectId, + isDirty, + shareableUrl, + urlService, + allowShortUrl: true, + // @ts-ignore this locator is passed mainly to test the code path that invokes createWithLocator + shareableUrlLocatorParams, + }); + + const copyButton = screen.getByTestId('copyShareUrlButton'); + + const numberOfClicks = 4; + + for (const _click of Array.from({ length: numberOfClicks })) { + await user.click(copyButton); + } + + // should only invoke once no matter how many times the button is clicked + expect(createWithLocatorSpy).toHaveBeenCalledTimes(1); + expect(copyButton.getAttribute('data-share-url')).toBe(shortURL); + }); + + it('invokes the createFromLongUrl method on the shortURL service if a locator is not present when the copy button is clicked', async () => { + const user = userEvent.setup(); + const objectType = 'dashboard'; + const objectId = '123'; + const isDirty = false; + + const shortURL = 'http://localhost:5601/xyz/r/s/yellow-orange-tomato'; + + const createFromLongUrlSpy = jest.spyOn(BrowserShortUrlClient.prototype, 'createFromLongUrl'); + + // @ts-expect-error we only return url property, as that's all we need for this test + createFromLongUrlSpy.mockResolvedValue({ + url: shortURL, + }); + + renderComponent({ + objectType, + objectId, + isDirty, + shareableUrl, + urlService, + allowShortUrl: true, + }); + + const copyButton = screen.getByTestId('copyShareUrlButton'); + + const numberOfClicks = 4; + + for (const _click of Array.from({ length: numberOfClicks })) { + await user.click(copyButton); + } + + // should only invoke once no matter how many times the button is clicked + expect(createFromLongUrlSpy).toHaveBeenCalledTimes(1); + expect(copyButton.getAttribute('data-share-url')).toBe(shortURL); + }); +}); diff --git a/src/plugins/share/public/components/tabs/link/link_content.tsx b/src/plugins/share/public/components/tabs/link/link_content.tsx index 0871599a524ba..6c0d8e6e988ec 100644 --- a/src/plugins/share/public/components/tabs/link/link_content.tsx +++ b/src/plugins/share/public/components/tabs/link/link_content.tsx @@ -20,8 +20,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo, useState } from 'react'; -import { IShareContext } from '../../context'; +import React, { useCallback, useState, useRef, useEffect } from 'react'; +import type { IShareContext } from '../../context'; type LinkProps = Pick< IShareContext, @@ -42,87 +42,80 @@ interface UrlParams { } export const LinkContent = ({ - objectType, isDirty, + objectType, shareableUrl, urlService, shareableUrlLocatorParams, allowShortUrl, delegatedShareUrlHandler, }: LinkProps) => { - const [url, setUrl] = useState(''); - const [urlParams] = useState(undefined); + const [snapshotUrl, setSnapshotUrl] = useState(''); const [isTextCopied, setTextCopied] = useState(false); - const [shortUrlCache, setShortUrlCache] = useState(undefined); const [isLoading, setIsLoading] = useState(false); + const urlParamsRef = useRef(undefined); + const urlToCopy = useRef(undefined); + const copiedTextToolTipCleanupIdRef = useRef>(); - const getUrlWithUpdatedParams = useCallback( - (tempUrl: string): string => { - const urlWithUpdatedParams = urlParams - ? Object.keys(urlParams).reduce((urlAccumulator, key) => { - const urlParam = urlParams[key]; - return urlParam - ? Object.keys(urlParam).reduce((queryAccumulator, queryParam) => { - const isQueryParamEnabled = urlParam[queryParam]; - return isQueryParamEnabled - ? queryAccumulator + `&${queryParam}=true` - : queryAccumulator; - }, urlAccumulator) - : urlAccumulator; - }, tempUrl) - : tempUrl; + const getUrlWithUpdatedParams = useCallback((tempUrl: string): string => { + const urlWithUpdatedParams = urlParamsRef.current + ? Object.keys(urlParamsRef.current).reduce((urlAccumulator, key) => { + const urlParam = urlParamsRef.current?.[key]; + return urlParam + ? Object.keys(urlParam).reduce((queryAccumulator, queryParam) => { + const isQueryParamEnabled = urlParam[queryParam]; + return isQueryParamEnabled + ? queryAccumulator + `&${queryParam}=true` + : queryAccumulator; + }, urlAccumulator) + : urlAccumulator; + }, tempUrl) + : tempUrl; - // persist updated url to state - setUrl(urlWithUpdatedParams); - return urlWithUpdatedParams; - }, - [urlParams] - ); + return urlWithUpdatedParams; + }, []); - const getSnapshotUrl = useCallback(() => { - return getUrlWithUpdatedParams(shareableUrl || window.location.href); + useEffect(() => { + setSnapshotUrl(getUrlWithUpdatedParams(shareableUrl || window.location.href)); }, [getUrlWithUpdatedParams, shareableUrl]); const createShortUrl = useCallback(async () => { + const shortUrlService = urlService.shortUrls.get(null); + if (shareableUrlLocatorParams) { - const shortUrls = urlService.shortUrls.get(null); - const shortUrl = await shortUrls.createWithLocator(shareableUrlLocatorParams); - const urlWithLoc = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true }); - setShortUrlCache(urlWithLoc); - return urlWithLoc; + const shortUrl = await shortUrlService.createWithLocator(shareableUrlLocatorParams); + return shortUrl.locator.getUrl(shortUrl.params, { absolute: true }); } else { - const snapshotUrl = getSnapshotUrl(); - const shortUrl = await urlService.shortUrls.get(null).createFromLongUrl(snapshotUrl); - setShortUrlCache(shortUrl.url); - - return shortUrl.url; + return (await shortUrlService.createFromLongUrl(snapshotUrl)).url; } - }, [shareableUrlLocatorParams, urlService.shortUrls, getSnapshotUrl, setShortUrlCache]); + }, [shareableUrlLocatorParams, urlService.shortUrls, snapshotUrl]); const copyUrlHelper = useCallback(async () => { setIsLoading(true); - let urlToCopy = url; - if (!urlToCopy || delegatedShareUrlHandler) { - urlToCopy = delegatedShareUrlHandler - ? delegatedShareUrlHandler?.() + if (!urlToCopy.current) { + urlToCopy.current = delegatedShareUrlHandler + ? delegatedShareUrlHandler() : allowShortUrl ? await createShortUrl() - : getSnapshotUrl(); + : snapshotUrl; } - copyToClipboard(urlToCopy); - setUrl(urlToCopy); - setTextCopied(true); + copyToClipboard(urlToCopy.current); + setTextCopied(() => { + if (copiedTextToolTipCleanupIdRef.current) { + clearInterval(copiedTextToolTipCleanupIdRef.current); + } + + // set up timer to revert copied state to false after specified duration + copiedTextToolTipCleanupIdRef.current = setTimeout(() => setTextCopied(false), 1000); + + // set copied state to true for now + return true; + }); setIsLoading(false); - return urlToCopy; - }, [url, delegatedShareUrlHandler, allowShortUrl, createShortUrl, getSnapshotUrl]); + }, [snapshotUrl, delegatedShareUrlHandler, allowShortUrl, createShortUrl]); - const handleTestUrl = useMemo(() => { - if (objectType !== 'search' || !allowShortUrl) return getSnapshotUrl(); - else if (objectType === 'search' && allowShortUrl) return shortUrlCache; - return copyUrlHelper(); - }, [objectType, getSnapshotUrl, allowShortUrl, shortUrlCache, copyUrlHelper]); return ( <> @@ -157,14 +150,14 @@ export const LinkContent = ({ (objectType === 'lens' && isDirty ? null : setTextCopied(false))} onClick={copyUrlHelper} color={objectType === 'lens' && isDirty ? 'warning' : 'primary'} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 511ab4cf89bfc..fe0f599dd6ca1 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3015,6 +3015,137 @@ } } }, + "entity_manager": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "appSearch": { "properties": { "appId": { @@ -7600,6 +7731,137 @@ } } }, + "streams": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "uptime": { "properties": { "appId": { diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 4fb1b9cbe6471..164d1eb539e3c 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -8,7 +8,6 @@ */ import React, { memo, ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; -import type { Observable } from 'rxjs'; import { Subject } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar'; @@ -70,7 +69,7 @@ export interface ChartProps { disabledActions?: LensEmbeddableInput['disabledActions']; input$?: UnifiedHistogramInput$; lensAdapters?: UnifiedHistogramChartLoadEvent['adapters']; - lensEmbeddableOutput$?: Observable; + dataLoading$?: LensEmbeddableOutput['dataLoading']; isChartLoading?: boolean; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; @@ -105,7 +104,7 @@ export function Chart({ disabledActions, input$: originalInput$, lensAdapters, - lensEmbeddableOutput$, + dataLoading$, isChartLoading, onChartHiddenChange, onTimeIntervalChange, @@ -383,9 +382,7 @@ export function Chart({ )} {canSaveVisualization && isSaveModalVisible && visContext.attributes && ( {}} onClose={() => setIsSaveModalVisible(false)} isSaveable={false} @@ -393,18 +390,16 @@ export function Chart({ )} {isFlyoutVisible && !!visContext && !!lensVisServiceCurrentSuggestionContext && ( )} diff --git a/src/plugins/unified_histogram/public/chart/chart_config_panel.tsx b/src/plugins/unified_histogram/public/chart/chart_config_panel.tsx index f2d080fcf0e6c..edcd831d3f7ac 100644 --- a/src/plugins/unified_histogram/public/chart/chart_config_panel.tsx +++ b/src/plugins/unified_histogram/public/chart/chart_config_panel.tsx @@ -8,7 +8,6 @@ */ import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react'; -import type { Observable } from 'rxjs'; import type { AggregateQuery, Query } from '@kbn/es-query'; import { isEqual, isObject } from 'lodash'; import type { LensEmbeddableOutput, Suggestion } from '@kbn/lens-plugin/public'; @@ -29,7 +28,7 @@ export function ChartConfigPanel({ services, visContext, lensAdapters, - lensEmbeddableOutput$, + dataLoading$, currentSuggestionContext, isFlyoutVisible, setIsFlyoutVisible, @@ -42,7 +41,7 @@ export function ChartConfigPanel({ isFlyoutVisible: boolean; setIsFlyoutVisible: (flag: boolean) => void; lensAdapters?: UnifiedHistogramChartLoadEvent['adapters']; - lensEmbeddableOutput$?: Observable; + dataLoading$?: LensEmbeddableOutput['dataLoading']; currentSuggestionContext: UnifiedHistogramSuggestionContext; isPlainRecord?: boolean; query?: Query | AggregateQuery; @@ -108,7 +107,7 @@ export function ChartConfigPanel({ updateSuggestion={updateSuggestion} updatePanelState={updatePanelState} lensAdapters={lensAdapters} - output$={lensEmbeddableOutput$} + dataLoading$={dataLoading$} displayFlyoutHeader closeFlyout={() => { setIsFlyoutVisible(false); @@ -141,7 +140,7 @@ export function ChartConfigPanel({ isFlyoutVisible, setIsFlyoutVisible, lensAdapters, - lensEmbeddableOutput$, + dataLoading$, currentSuggestionType, ]); diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index 72b5c0cc0b791..7bef5d4f85554 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { Histogram } from './histogram'; import React from 'react'; -import { of, Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { getLensVisMock } from '../__mocks__/lens_vis'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; @@ -101,7 +101,7 @@ describe('Histogram', () => { searchSessionId: props.request.searchSessionId, getTimeRange: props.getTimeRange, attributes: (await getMockLensAttributes())!.attributes, - onLoad: lensProps.onLoad, + onLoad: lensProps.onLoad!, }); expect(lensProps).toMatchObject(expect.objectContaining(originalProps)); component.setProps({ request: { ...props.request, searchSessionId: '321' } }).update(); @@ -120,7 +120,7 @@ describe('Histogram', () => { it('should execute onLoad correctly', async () => { const { component, props } = await mountComponent(); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; - const onLoad = component.find(embeddable).props().onLoad; + const onLoad = component.find(embeddable).props().onLoad!; const adapters = createDefaultInspectorAdapters(); adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; const rawResponse = { @@ -172,25 +172,25 @@ describe('Histogram', () => { jest .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ response: { json: { rawResponse } } } as any]); - const embeddableOutput$ = jest.fn().mockReturnValue(of('output$')); - onLoad(true, undefined, embeddableOutput$); + const dataLoading$ = new BehaviorSubject(false); + onLoad(true, undefined, dataLoading$); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.loading, undefined ); - expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters: {}, embeddableOutput$ }); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters: {}, dataLoading$ }); expect(buildBucketInterval.buildBucketInterval).not.toHaveBeenCalled(); expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( expect.objectContaining({ bucketInterval: undefined }) ); act(() => { - onLoad(false, adapters, embeddableOutput$); + onLoad?.(false, adapters, dataLoading$); }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.complete, 100 ); - expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters, embeddableOutput$ }); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters, dataLoading$ }); expect(buildBucketInterval.buildBucketInterval).toHaveBeenCalled(); expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( expect.objectContaining({ bucketInterval: mockBucketInterval }) @@ -200,12 +200,12 @@ describe('Histogram', () => { it('should execute onLoad correctly when the request has a failure status', async () => { const { component, props } = await mountComponent(); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; - const onLoad = component.find(embeddable).props().onLoad; + const onLoad = component.find(embeddable).props().onLoad!; const adapters = createDefaultInspectorAdapters(); jest .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ status: RequestStatus.ERROR } as any]); - onLoad(false, adapters); + onLoad?.(false, adapters); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.error, undefined @@ -216,7 +216,7 @@ describe('Histogram', () => { it('should execute onLoad correctly when the response has shard failures', async () => { const { component, props } = await mountComponent(); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; - const onLoad = component.find(embeddable).props().onLoad; + const onLoad = component.find(embeddable).props().onLoad!; const adapters = createDefaultInspectorAdapters(); adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; const rawResponse = { @@ -237,7 +237,7 @@ describe('Histogram', () => { .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ response: { json: { rawResponse } } } as any]); act(() => { - onLoad(false, adapters); + onLoad?.(false, adapters); }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.error, @@ -249,7 +249,7 @@ describe('Histogram', () => { it('should execute onLoad correctly for textbased language and no Lens suggestions', async () => { const { component, props } = await mountComponent(true, false); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; - const onLoad = component.find(embeddable).props().onLoad; + const onLoad = component.find(embeddable).props().onLoad!; const adapters = createDefaultInspectorAdapters(); adapters.tables.tables.layerId = { meta: { type: 'es_ql' }, @@ -273,7 +273,7 @@ describe('Histogram', () => { ], } as any; act(() => { - onLoad(false, adapters); + onLoad?.(false, adapters); }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.complete, @@ -285,7 +285,7 @@ describe('Histogram', () => { it('should execute onLoad correctly for textbased language and Lens suggestions', async () => { const { component, props } = await mountComponent(true, true); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; - const onLoad = component.find(embeddable).props().onLoad; + const onLoad = component.find(embeddable).props().onLoad!; const adapters = createDefaultInspectorAdapters(); adapters.tables.tables.layerId = { meta: { type: 'es_ql' }, @@ -309,7 +309,7 @@ describe('Histogram', () => { ], } as any; act(() => { - onLoad(false, adapters); + onLoad?.(false, adapters); }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( UnifiedHistogramFetchStatus.complete, diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 7e8c6ea382bd4..8e3aa78da8d9d 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -10,18 +10,15 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useState } from 'react'; -import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters, Datatable } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/search-types'; import type { estypes } from '@elastic/elasticsearch'; import type { TimeRange } from '@kbn/es-query'; -import { - EmbeddableComponentProps, - LensEmbeddableInput, - LensEmbeddableOutput, -} from '@kbn/lens-plugin/public'; +import type { EmbeddableComponentProps, LensEmbeddableInput } from '@kbn/lens-plugin/public'; import { RequestStatus } from '@kbn/inspector-plugin/public'; import type { Observable } from 'rxjs'; +import { PublishingSubject } from '@kbn/presentation-publishing'; import { UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, @@ -59,32 +56,6 @@ export interface HistogramProps { withDefaultActions: EmbeddableComponentProps['withDefaultActions']; } -/** - * To prevent flakiness in the chart, we need to ensure that the data view config is valid. - * This requires that there are not multiple different data view ids in the given configuration. - * @param dataView - * @param visContext - * @param adHocDataViews - */ -const checkValidDataViewConfig = ( - dataView: DataView, - visContext: UnifiedHistogramVisContext, - adHocDataViews: { [key: string]: DataViewSpec } | undefined -) => { - if (!dataView.id) { - return false; - } - - if (!dataView.isPersisted() && !adHocDataViews?.[dataView.id]) { - return false; - } - - if (dataView.id !== visContext.requestData.dataViewId) { - return false; - } - return true; -}; - const computeTotalHits = ( hasLensSuggestions: boolean, adapterTables: @@ -147,7 +118,7 @@ export function Histogram({ ( isLoading: boolean, adapters: Partial | undefined, - lensEmbeddableOutput$?: Observable + dataLoading$?: PublishingSubject ) => { const lensRequest = adapters?.requests?.getRequests()[0]; const requestFailed = lensRequest?.status === RequestStatus.ERROR; @@ -186,7 +157,7 @@ export function Histogram({ setBucketInterval(newBucketInterval); } - onChartLoad?.({ adapters: adapters ?? {}, embeddableOutput$: lensEmbeddableOutput$ }); + onChartLoad?.({ adapters: adapters ?? {}, dataLoading$ }); } ); @@ -230,10 +201,6 @@ export function Histogram({ } `; - if (!checkValidDataViewConfig(dataView, visContext, lensProps.attributes.state.adHocDataViews)) { - return <>; - } - return ( <>
{ "hidden": false, "timeInterval": "auto", }, + "dataLoading$": undefined, "hits": Object { "status": "uninitialized", "total": undefined, @@ -120,7 +121,6 @@ describe('useStateProps', () => { }, }, }, - "lensEmbeddableOutput$": undefined, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -164,6 +164,7 @@ describe('useStateProps', () => { "hidden": false, "timeInterval": "auto", }, + "dataLoading$": undefined, "hits": Object { "status": "uninitialized", "total": undefined, @@ -204,7 +205,6 @@ describe('useStateProps', () => { }, }, }, - "lensEmbeddableOutput$": undefined, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -348,6 +348,7 @@ describe('useStateProps', () => { Object { "breakdown": undefined, "chart": undefined, + "dataLoading$": undefined, "hits": Object { "status": "uninitialized", "total": undefined, @@ -388,7 +389,6 @@ describe('useStateProps', () => { }, }, }, - "lensEmbeddableOutput$": undefined, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -427,6 +427,7 @@ describe('useStateProps', () => { Object { "breakdown": undefined, "chart": undefined, + "dataLoading$": undefined, "hits": Object { "status": "uninitialized", "total": undefined, @@ -467,7 +468,6 @@ describe('useStateProps', () => { }, }, }, - "lensEmbeddableOutput$": undefined, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts index fcc19fcd78a00..660e47f33cf0c 100644 --- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts +++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts @@ -27,7 +27,7 @@ import { totalHitsResultSelector, totalHitsStatusSelector, lensAdaptersSelector, - lensEmbeddableOutputSelector$, + lensDataLoadingSelector$, } from '../utils/state_selectors'; import { useStateSelector } from '../utils/use_state_selector'; @@ -52,10 +52,7 @@ export const useStateProps = ({ const totalHitsResult = useStateSelector(stateService?.state$, totalHitsResultSelector); const totalHitsStatus = useStateSelector(stateService?.state$, totalHitsStatusSelector); const lensAdapters = useStateSelector(stateService?.state$, lensAdaptersSelector); - const lensEmbeddableOutput$ = useStateSelector( - stateService?.state$, - lensEmbeddableOutputSelector$ - ); + const lensDataLoading$ = useStateSelector(stateService?.state$, lensDataLoadingSelector$); /** * Contexts */ @@ -162,7 +159,7 @@ export const useStateProps = ({ // We need to store the Lens request adapter in order to inspect its requests stateService?.setLensRequestAdapter(event.adapters.requests); stateService?.setLensAdapters(event.adapters); - stateService?.setLensEmbeddableOutput$(event.embeddableOutput$); + stateService?.setLensDataLoading$(event.dataLoading$); }, [stateService] ); @@ -199,7 +196,7 @@ export const useStateProps = ({ request, isPlainRecord, lensAdapters, - lensEmbeddableOutput$, + dataLoading$: lensDataLoading$, onTopPanelHeightChange, onTimeIntervalChange, onTotalHitsChange, diff --git a/src/plugins/unified_histogram/public/container/services/state_service.test.ts b/src/plugins/unified_histogram/public/container/services/state_service.test.ts index dcce90037ec99..66f0549e9571f 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.test.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.test.ts @@ -139,8 +139,8 @@ describe('UnifiedHistogramStateService', () => { stateService.setLensAdapters(undefined); newState = { ...newState, lensAdapters: undefined }; expect(state).toEqual(newState); - stateService.setLensEmbeddableOutput$(undefined); - newState = { ...newState, lensEmbeddableOutput$: undefined }; + stateService.setLensDataLoading$(undefined); + newState = { ...newState, dataLoading$: undefined }; expect(state).toEqual(newState); stateService.setTotalHits({ totalHitsStatus: UnifiedHistogramFetchStatus.complete, diff --git a/src/plugins/unified_histogram/public/container/services/state_service.ts b/src/plugins/unified_histogram/public/container/services/state_service.ts index 551773cfe1892..c3cf82bf94578 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.ts @@ -8,8 +8,8 @@ */ import type { RequestAdapter } from '@kbn/inspector-plugin/common'; -import type { LensEmbeddableOutput } from '@kbn/lens-plugin/public'; import { BehaviorSubject, Observable } from 'rxjs'; +import { PublishingSubject } from '@kbn/presentation-publishing'; import { UnifiedHistogramFetchStatus } from '../..'; import type { UnifiedHistogramServices, UnifiedHistogramChartLoadEvent } from '../../types'; import { @@ -49,7 +49,7 @@ export interface UnifiedHistogramState { /** * Lens embeddable output observable */ - lensEmbeddableOutput$?: Observable; + dataLoading$?: PublishingSubject; /** * The current time interval of the chart */ @@ -124,9 +124,7 @@ export interface UnifiedHistogramStateService { * Sets the current Lens adapters */ setLensAdapters: (lensAdapters: UnifiedHistogramChartLoadEvent['adapters'] | undefined) => void; - setLensEmbeddableOutput$: ( - lensEmbeddableOutput$: Observable | undefined - ) => void; + setLensDataLoading$: (dataLoading$: PublishingSubject | undefined) => void; /** * Sets the current total hits status and result */ @@ -214,10 +212,8 @@ export const createStateService = ( setLensAdapters: (lensAdapters: UnifiedHistogramChartLoadEvent['adapters'] | undefined) => { updateState({ lensAdapters }); }, - setLensEmbeddableOutput$: ( - lensEmbeddableOutput$: Observable | undefined - ) => { - updateState({ lensEmbeddableOutput$ }); + setLensDataLoading$: (dataLoading$: PublishingSubject | undefined) => { + updateState({ dataLoading$ }); }, setTotalHits: (totalHits: { diff --git a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts index 6eacbaaef9500..9274c4fabd301 100644 --- a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts +++ b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts @@ -16,5 +16,4 @@ export const topPanelHeightSelector = (state: UnifiedHistogramState) => state.to export const totalHitsResultSelector = (state: UnifiedHistogramState) => state.totalHitsResult; export const totalHitsStatusSelector = (state: UnifiedHistogramState) => state.totalHitsStatus; export const lensAdaptersSelector = (state: UnifiedHistogramState) => state.lensAdapters; -export const lensEmbeddableOutputSelector$ = (state: UnifiedHistogramState) => - state.lensEmbeddableOutput$; +export const lensDataLoadingSelector$ = (state: UnifiedHistogramState) => state.dataLoading$; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 3e34cf4ee69b3..b9d9f6fbc446f 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -9,7 +9,6 @@ import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui'; import React, { PropsWithChildren, ReactElement, useEffect, useMemo, useState } from 'react'; -import { Observable } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; @@ -99,7 +98,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren */ hits?: UnifiedHistogramHitsContext; lensAdapters?: UnifiedHistogramChartLoadEvent['adapters']; - lensEmbeddableOutput$?: Observable; + dataLoading$?: LensEmbeddableOutput['dataLoading']; /** * Context object for the chart -- leave undefined to hide the chart */ @@ -214,7 +213,7 @@ export const UnifiedHistogramLayout = ({ request, hits, lensAdapters, - lensEmbeddableOutput$, + dataLoading$, chart: originalChart, breakdown, container, @@ -372,7 +371,7 @@ export const UnifiedHistogramLayout = ({ onFilter={onFilter} onBrushEnd={onBrushEnd} lensAdapters={lensAdapters} - lensEmbeddableOutput$={lensEmbeddableOutput$} + dataLoading$={dataLoading$} withDefaultActions={withDefaultActions} columns={columns} /> diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.attributes.test.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.attributes.test.ts index babea0335e1c3..f338ef955c01e 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.attributes.test.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.attributes.test.ts @@ -108,6 +108,7 @@ describe('LensVisService attributes', () => { "sourceField": "timestamp", }, }, + "indexPatternId": "index-pattern-with-timefield-id", }, }, }, @@ -284,6 +285,7 @@ describe('LensVisService attributes', () => { "sourceField": "timestamp", }, }, + "indexPatternId": "index-pattern-with-timefield-id", }, }, }, @@ -434,6 +436,7 @@ describe('LensVisService attributes', () => { "sourceField": "timestamp", }, }, + "indexPatternId": "index-pattern-with-timefield-id", }, }, }, diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.ts index 1f119ee5b1c92..5342ef4723b13 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.ts @@ -403,7 +403,7 @@ export class LensVisService { const datasourceState = { layers: { - [UNIFIED_HISTOGRAM_LAYER_ID]: { columnOrder, columns }, + [UNIFIED_HISTOGRAM_LAYER_ID]: { columnOrder, columns, indexPatternId: dataView.id }, }, }; diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index b777fe89a348e..a64000da11df0 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -10,19 +10,15 @@ import type { IUiSettingsClient, Capabilities } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { - LensEmbeddableOutput, - LensPublicStart, - TypedLensByValueInput, - Suggestion, -} from '@kbn/lens-plugin/public'; +import type { LensPublicStart, TypedLensByValueInput, Suggestion } from '@kbn/lens-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; import type { RequestAdapter } from '@kbn/inspector-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; -import type { Observable, Subject } from 'rxjs'; +import type { Subject } from 'rxjs'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import { PublishingSubject } from '@kbn/presentation-publishing'; /** * The fetch status of a Unified Histogram request @@ -72,9 +68,9 @@ export interface UnifiedHistogramChartLoadEvent { */ adapters: UnifiedHistogramAdapters; /** - * Observable of the lens embeddable output + * Observable for the data change subscription */ - embeddableOutput$?: Observable; + dataLoading$?: PublishingSubject; } /** diff --git a/src/plugins/unified_histogram/public/utils/external_vis_context.ts b/src/plugins/unified_histogram/public/utils/external_vis_context.ts index ef5788b4b25ba..29e393d145087 100644 --- a/src/plugins/unified_histogram/public/utils/external_vis_context.ts +++ b/src/plugins/unified_histogram/public/utils/external_vis_context.ts @@ -43,7 +43,7 @@ export const exportVisContext = ( ? { suggestionType: visContext.suggestionType, requestData: visContext.requestData, - attributes: removeTablesFromLensAttributes(visContext.attributes), + attributes: removeTablesFromLensAttributes(visContext.attributes).attributes, } : undefined; diff --git a/src/plugins/unified_histogram/public/utils/lens_vis_from_table.ts b/src/plugins/unified_histogram/public/utils/lens_vis_from_table.ts index 95693851db52e..bc618343a0a70 100644 --- a/src/plugins/unified_histogram/public/utils/lens_vis_from_table.ts +++ b/src/plugins/unified_histogram/public/utils/lens_vis_from_table.ts @@ -10,6 +10,7 @@ import type { Datatable } from '@kbn/expressions-plugin/common'; import type { LensAttributes } from '@kbn/lens-embeddable-utils'; import type { TextBasedPersistedState } from '@kbn/lens-plugin/public/datasources/text_based/types'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; export const enrichLensAttributesWithTablesData = ({ attributes, @@ -53,6 +54,8 @@ export const enrichLensAttributesWithTablesData = ({ return updatedAttributes; }; -export const removeTablesFromLensAttributes = (attributes: LensAttributes): LensAttributes => { - return enrichLensAttributesWithTablesData({ attributes, table: undefined }); +export const removeTablesFromLensAttributes = ( + attributes: LensAttributes +): TypedLensByValueInput => { + return { attributes: enrichLensAttributesWithTablesData({ attributes, table: undefined }) }; }; diff --git a/src/plugins/unified_histogram/tsconfig.json b/src/plugins/unified_histogram/tsconfig.json index d14adf53889b9..68c096665eb79 100644 --- a/src/plugins/unified_histogram/tsconfig.json +++ b/src/plugins/unified_histogram/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/discover-utils", "@kbn/visualization-utils", "@kbn/search-types", + "@kbn/presentation-publishing", "@kbn/data-view-utils", ], "exclude": [ diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx index ae51b96522c27..0abc6587b8726 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx @@ -155,7 +155,7 @@ describe('Saved query management list component', () => { it('should render the saved queries on the selectable component', async () => { render(wrapSavedQueriesListComponentInContext(props)); - expect(await screen.findAllByRole('option')).toHaveLength(1); + await waitFor(() => expect(screen.queryAllByRole('option')).toHaveLength(1)); expect(screen.getByRole('option', { name: 'Test' })).toBeInTheDocument(); }); diff --git a/src/plugins/vis_types/table/public/utils/use/use_pagination.test.ts b/src/plugins/vis_types/table/public/utils/use/use_pagination.test.ts index be6ea0a4427c2..d6ac1fbebc5f7 100644 --- a/src/plugins/vis_types/table/public/utils/use/use_pagination.test.ts +++ b/src/plugins/vis_types/table/public/utils/use/use_pagination.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { AggTypes } from '../../../common'; import { usePagination } from './use_pagination'; diff --git a/src/plugins/vis_types/table/public/utils/use/use_ui_state.test.ts b/src/plugins/vis_types/table/public/utils/use/use_ui_state.test.ts index 37525bf12626b..a6a86426a2f4b 100644 --- a/src/plugins/vis_types/table/public/utils/use/use_ui_state.test.ts +++ b/src/plugins/vis_types/table/public/utils/use/use_ui_state.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook, act } from '@testing-library/react'; import type { PersistedState } from '@kbn/visualizations-plugin/public'; import { TableVisUiState } from '../../types'; import { useUiState } from './use_ui_state'; @@ -39,7 +39,7 @@ describe('useUiState', () => { }); it('should subscribe on uiState changes and update local state', async () => { - const { result, unmount, waitForNextUpdate } = renderHook(() => useUiState(uiState)); + const { result, unmount } = renderHook(() => useUiState(uiState)); expect(uiState.on).toHaveBeenCalledWith('change', expect.any(Function)); // @ts-expect-error @@ -61,18 +61,18 @@ describe('useUiState', () => { updateOnChange(); }); - await waitForNextUpdate(); - // should update local state with new values - expect(result.current).toEqual({ - columnsWidth: [], - sort: { - columnIndex: 1, - direction: 'asc', - }, - setColumnsWidth: expect.any(Function), - setSort: expect.any(Function), - }); + await waitFor(() => + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }) + ); act(() => { updateOnChange(); diff --git a/src/plugins/visualizations/public/embeddable/types.ts b/src/plugins/visualizations/public/embeddable/types.ts index f4215d923e1d5..80e7e2d9179e8 100644 --- a/src/plugins/visualizations/public/embeddable/types.ts +++ b/src/plugins/visualizations/public/embeddable/types.ts @@ -17,6 +17,7 @@ import { HasSupportedTriggers, PublishesDataLoading, PublishesDataViews, + PublishesRendered, PublishesTimeRange, SerializedTimeRange, SerializedTitles, @@ -92,6 +93,7 @@ export const isVisualizeRuntimeState = (state: unknown): state is VisualizeRunti export type VisualizeApi = Partial & PublishesDataViews & PublishesDataLoading & + PublishesRendered & HasVisualizeConfig & HasInspectorAdapters & HasSupportedTriggers & diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 7b48521265d6f..4993c0168313a 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -179,6 +179,7 @@ export const getVisualizeEmbeddableFactory: (deps: { defaultPanelTitle, dataLoading: dataLoading$, dataViews: new BehaviorSubject(initialDataViews), + rendered$: hasRendered$, supportedTriggers: () => [ ACTION_CONVERT_TO_LENS, APPLY_FILTER_TRIGGER, @@ -397,7 +398,6 @@ export const getVisualizeEmbeddableFactory: (deps: { if (hasRendered$.getValue() === true) return; hasRendered$.next(true); - hasRendered$.complete(); }, onEvent: async (event) => { // Visualize doesn't respond to sizing events, so ignore. diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_chrome_visibility.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_chrome_visibility.test.ts index 08c2015913908..01d90ee2b2a2b 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_chrome_visibility.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_chrome_visibility.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { chromeServiceMock } from '@kbn/core/public/mocks'; import { useChromeVisibility } from './use_chrome_visibility'; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.test.ts index c0f6719a403fe..b96185a0270a4 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { EventEmitter } from 'events'; import { useEditorUpdates } from './use_editor_updates'; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.test.ts index 8cf124ca243cb..d979021b74888 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { EventEmitter } from 'events'; import { useLinkedSearchUpdates } from './use_linked_search_updates'; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts index 7c32fec754428..c5131460d76b7 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { EventEmitter } from 'events'; import { setTypes } from '../../../services'; @@ -127,7 +127,7 @@ describe('useSavedVisInstance', () => { describe('edit saved visualization route', () => { test('should load instance and initiate an editor if chrome is set up', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, savedVisId) ); @@ -135,7 +135,7 @@ describe('useSavedVisInstance', () => { expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); expect(mockServices.chrome.docTitle.change).toHaveBeenCalledWith('Test Vis'); expect(getEditBreadcrumbs).toHaveBeenCalledWith( @@ -156,7 +156,7 @@ describe('useSavedVisInstance', () => { }, id: 'panel1', }; - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSavedVisInstance( mockServices, eventEmitter, @@ -171,7 +171,7 @@ describe('useSavedVisInstance', () => { expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); expect(mockServices.chrome.docTitle.change).toHaveBeenCalledWith('Test Vis'); expect(getEditBreadcrumbs).toHaveBeenCalledWith( @@ -189,13 +189,13 @@ describe('useSavedVisInstance', () => { }); test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { - const { result, unmount, waitForNextUpdate } = renderHook(() => + const { result, unmount } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, savedVisId) ); result.current.visEditorRef.current = document.createElement('div'); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); unmount(); expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1); @@ -215,7 +215,7 @@ describe('useSavedVisInstance', () => { }); test('should create new visualization based on search params', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined, undefined) ); @@ -226,7 +226,7 @@ describe('useSavedVisInstance', () => { type: 'area', }); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(getCreateBreadcrumbs).toHaveBeenCalled(); expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); @@ -263,7 +263,7 @@ describe('useSavedVisInstance', () => { describe('embeded mode', () => { test('should create new visualization based on search params', async () => { - const { result, unmount, waitForNextUpdate } = renderHook(() => + const { result, unmount } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, false, undefined, savedVisId) ); @@ -273,7 +273,7 @@ describe('useSavedVisInstance', () => { expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(mockEmbeddableHandlerRender).toHaveBeenCalled(); expect(result.current.visEditorController).toBeUndefined(); diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.test.ts index f92bd7304a7e8..7add55761e2af 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { EventEmitter } from 'events'; import { Observable } from 'rxjs'; @@ -159,11 +159,11 @@ describe('useVisualizeAppState', () => { it('should successfully update vis state and set up app state container', async () => { stateContainerGetStateMock.mockImplementation(() => state); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { aggs, ...visState } = stateContainer.getState().vis; const expectedNewVisState = { @@ -183,11 +183,11 @@ describe('useVisualizeAppState', () => { ...visualizeAppStateStub, query: { query: 'test', language: 'kuery' }, })); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { aggs, ...visState } = stateContainer.getState().vis; const expectedNewVisState = { diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts b/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts index 35b7db4fabfc6..faaacc48fe97c 100644 --- a/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts +++ b/test/functional/apps/dashboard/group6/dashboard_esql_chart.ts @@ -18,6 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const monacoEditor = getService('monacoEditor'); const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const log = getService('log'); describe('dashboard add ES|QL chart', function () { before(async () => { @@ -30,6 +32,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + after(async () => { + await dashboard.navigateToApp(); + await testSubjects.click('discard-unsaved-New-Dashboard'); + }); + it('should add an ES|QL datatable chart when the ES|QL panel action is clicked', async () => { await dashboard.navigateToApp(); await dashboard.clickNewDashboard(); @@ -57,6 +64,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + it('should reset to the previous state on edit inline', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + await dashboard.waitForRenderComplete(); + + // Save the panel and close the flyout + log.debug('Applies the changes'); + await testSubjects.click('applyFlyoutButton'); + + // now edit the panel and click on Cancel + await dashboardPanelActions.clickInlineEdit(); + + const metricsConfigured = await testSubjects.findAll( + 'lnsDatatable_metrics > lnsLayerPanel-dimensionLink' + ); + // remove the first metric from the configuration + // Lens is x-pack so not available here, make things manually + await testSubjects.moveMouseTo(`lnsDatatable_metrics > indexPattern-dimension-remove`); + await testSubjects.click(`lnsDatatable_metrics > indexPattern-dimension-remove`); + const beforeCancelMetricsConfigured = await testSubjects.findAll( + 'lnsDatatable_metrics > lnsLayerPanel-dimensionLink' + ); + expect(beforeCancelMetricsConfigured.length).to.eql(metricsConfigured.length - 1); + + // now click cancel + await testSubjects.click('cancelFlyoutButton'); + await dashboard.waitForRenderComplete(); + + // re open the inline editor and check that the configured metrics are still the original ones + await dashboardPanelActions.clickInlineEdit(); + const afterCancelMetricsConfigured = await testSubjects.findAll( + 'lnsDatatable_metrics > lnsLayerPanel-dimensionLink' + ); + expect(afterCancelMetricsConfigured.length).to.eql(metricsConfigured.length); + // delete the panel + await testSubjects.click('cancelFlyoutButton'); + const panels = await dashboard.getDashboardPanels(); + await dashboardPanelActions.removePanel(panels[0]); + }); + it('should be able to edit the query and render another chart', async () => { await dashboardAddPanel.clickEditorMenuButton(); await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); @@ -70,5 +118,41 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('applyFlyoutButton'); expect(await testSubjects.exists('mtrVis')).to.be(true); }); + + it('should add a second panel and remove when hitting cancel', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + await dashboard.waitForRenderComplete(); + // Cancel + await testSubjects.click('cancelFlyoutButton'); + // Test that there's only 1 panel left + await dashboard.waitForRenderComplete(); + await retry.try(async () => { + const panelCount = await dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + }); + + it('should not remove the first panel of two when editing and cancelling', async () => { + // add a second panel + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + await dashboard.waitForRenderComplete(); + // save it + await testSubjects.click('applyFlyoutButton'); + await dashboard.waitForRenderComplete(); + + // now edit the first one + const [firstPanel] = await dashboard.getDashboardPanels(); + await dashboardPanelActions.clickInlineEdit(firstPanel); + await testSubjects.click('cancelFlyoutButton'); + await dashboard.waitForRenderComplete(); + await retry.try(async () => { + const panelCount = await dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + }); }); } diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts index 333ac7f015397..4298ccdfb5886 100644 --- a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts +++ b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.expectOnDashboard('New Dashboard'); expect(await testSubjects.exists('lnsVisualizationContainer')).to.be(true); - await panelActions.clickInlineEdit(); + await panelActions.clickEdit(); const editorValue = await monacoEditor.getCodeEditorValue(); expect(editorValue).to.eql(`FROM logs* | LIMIT 10`); }); diff --git a/test/functional/apps/dashboard/group6/view_edit.ts b/test/functional/apps/dashboard/group6/view_edit.ts index 487adc753e652..9304b51d302d5 100644 --- a/test/functional/apps/dashboard/group6/view_edit.ts +++ b/test/functional/apps/dashboard/group6/view_edit.ts @@ -25,7 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const security = getService('security'); - describe('dashboard view edit mode', function viewEditModeTests() { + // Failing: See https://github.com/elastic/kibana/issues/200748 + describe.skip('dashboard view edit mode', function viewEditModeTests() { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.load( diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts index d27df2244b18b..65d8b7ce698b6 100644 --- a/test/functional/apps/discover/esql/_esql_view.ts +++ b/test/functional/apps/discover/esql/_esql_view.ts @@ -383,6 +383,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await testSubjects.click('querySubmitButton'); await header.waitUntilLoadingHasFinished(); + // for some reason the chart query is taking a very long time to return (3x the delay) + // so wait for the chart to be loaded + await discover.waitForChartLoadingComplete(1); await browser.execute(() => { window.ELASTIC_ESQL_DELAY_SECONDS = undefined; }); diff --git a/test/functional/apps/discover/group3/_request_counts.ts b/test/functional/apps/discover/group3/_request_counts.ts index 8a029928af0cb..32f1be5a62e79 100644 --- a/test/functional/apps/discover/group3/_request_counts.ts +++ b/test/functional/apps/discover/group3/_request_counts.ts @@ -97,7 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expectedRequests?: number; expectedRefreshRequest?: number; }) => { - it(`should send ${expectedRequests} search requests (documents + chart) on page load`, async () => { + it(`should send no more than ${expectedRequests} search requests (documents + chart) on page load`, async () => { await browser.refresh(); await browser.execute(async () => { performance.setResourceTimingBufferSize(Number.MAX_SAFE_INTEGER); @@ -107,20 +107,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(searchCount).to.be(expectedRequests); }); - it(`should send ${expectedRequests} requests (documents + chart) when refreshing`, async () => { + it(`should send no more than ${expectedRequests} requests (documents + chart) when refreshing`, async () => { await expectSearches(type, expectedRequests, async () => { await queryBar.clickQuerySubmitButton(); }); }); - it(`should send ${expectedRequests} requests (documents + chart) when changing the query`, async () => { + it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the query`, async () => { await expectSearches(type, expectedRequests, async () => { await setQuery(query1); await queryBar.clickQuerySubmitButton(); }); }); - it(`should send ${expectedRequests} requests (documents + chart) when changing the time range`, async () => { + it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the time range`, async () => { await expectSearches(type, expectedRequests, async () => { await timePicker.setAbsoluteRange( 'Sep 21, 2015 @ 06:31:44.000', @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { setQuery: (query) => queryBar.setQuery(query), }); - it(`should send 2 requests (documents + chart) when toggling the chart visibility`, async () => { + it(`should send no more than 2 requests (documents + chart) when toggling the chart visibility`, async () => { await expectSearches(type, 2, async () => { await discover.toggleChartVisibility(); }); @@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should send 2 requests (documents + chart) when adding a filter', async () => { + it('should send no more than 2 requests (documents + chart) when adding a filter', async () => { await expectSearches(type, 2, async () => { await filterBar.addFilter({ field: 'extension', @@ -193,31 +193,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should send 2 requests (documents + chart) when sorting', async () => { + it('should send no more than 2 requests (documents + chart) when sorting', async () => { await expectSearches(type, 2, async () => { await discover.clickFieldSort('@timestamp', 'Sort Old-New'); }); }); - it('should send 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => { + it('should send no more than 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => { await expectSearches(type, 2, async () => { await discover.chooseBreakdownField('type'); }); }); - it('should send 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => { + it('should send no more than 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => { await expectSearches(type, 3, async () => { await discover.chooseBreakdownField('extension.raw'); }); }); - it('should send 2 requests (documents + chart) when changing the chart interval', async () => { + it('should send no more than 2 requests (documents + chart) when changing the chart interval', async () => { await expectSearches(type, 2, async () => { await discover.setChartInterval('Day'); }); }); - it('should send 2 requests (documents + chart) when changing the data view', async () => { + it('should send no more than 2 requests (documents + chart) when changing the data view', async () => { await expectSearches(type, 2, async () => { await discover.selectIndexPattern('long-window-logstash-*'); }); diff --git a/test/functional/apps/discover/group4/_data_view_edit.ts b/test/functional/apps/discover/group4/_data_view_edit.ts index c0c5216010191..809902857eac5 100644 --- a/test/functional/apps/discover/group4/_data_view_edit.ts +++ b/test/functional/apps/discover/group4/_data_view_edit.ts @@ -25,7 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'unifiedFieldList', ]); - describe('data view flyout', function () { + // Failing: See https://github.com/elastic/kibana/issues/201071 + describe.skip('data view flyout', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); diff --git a/test/functional/apps/visualize/group3/_annotation_listing.ts b/test/functional/apps/visualize/group3/_annotation_listing.ts index a6a1743430092..1ec8fb8cdea97 100644 --- a/test/functional/apps/visualize/group3/_annotation_listing.ts +++ b/test/functional/apps/visualize/group3/_annotation_listing.ts @@ -177,7 +177,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataView: 'logs*', }); expect(await annotationEditor.showingMissingDataViewPrompt()).to.be(false); - expect(await find.byCssSelector('canvas')).to.be.ok(); + // @TODO: re-enable this once the error bubbling issue is fixed at Lens custom component level + // expect(await find.byCssSelector('canvas')).to.be.ok(); }); await annotationEditor.saveGroup(); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 75474fef41655..31890d4c4c478 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -12,7 +12,6 @@ import { FtrService } from '../../ftr_provider_context'; const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; -const INLINE_EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CONFIGURE_IN_LENS'; const EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ = 'navigateToLensEditorLink'; const CLONE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-clonePanel'; const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; @@ -128,7 +127,9 @@ export class DashboardPanelActionsService extends FtrService { async navigateToEditorFromFlyout(wrapper?: WebElementWrapper) { this.log.debug('navigateToEditorFromFlyout'); - await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ, wrapper); + // make sure the context menu is open before proceeding + await this.openContextMenu(); + await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); await this.header.waitUntilLoadingHasFinished(); await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ); const isConfirmModalVisible = await this.testSubjects.exists('confirmModalConfirmButton'); @@ -139,9 +140,9 @@ export class DashboardPanelActionsService extends FtrService { } } - async clickInlineEdit() { + async clickInlineEdit(wrapper?: WebElementWrapper) { this.log.debug('clickInlineEditAction'); - await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); + await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, wrapper); await this.header.waitUntilLoadingHasFinished(); await this.common.waitForTopNavToBeVisible(); } @@ -307,12 +308,9 @@ export class DashboardPanelActionsService extends FtrService { await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ, title); } - async expectExistsEditPanelAction(title = '', allowsInlineEditing?: boolean) { + async expectExistsEditPanelAction(title = '') { this.log.debug('expectExistsEditPanelAction'); - let testSubj = EDIT_PANEL_DATA_TEST_SUBJ; - if (allowsInlineEditing) { - testSubj = INLINE_EDIT_PANEL_DATA_TEST_SUBJ; - } + const testSubj = EDIT_PANEL_DATA_TEST_SUBJ; await this.expectExistsPanelAction(testSubj, title); } diff --git a/tsconfig.base.json b/tsconfig.base.json index bf11b49d4e17d..9dd73f15f8d1b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -756,6 +756,8 @@ "@kbn/default-nav-management/*": ["packages/default-nav/management/*"], "@kbn/default-nav-ml": ["packages/default-nav/ml"], "@kbn/default-nav-ml/*": ["packages/default-nav/ml/*"], + "@kbn/dependency-usage": ["packages/kbn-dependency-usage"], + "@kbn/dependency-usage/*": ["packages/kbn-dependency-usage/*"], "@kbn/dev-cli-errors": ["packages/kbn-dev-cli-errors"], "@kbn/dev-cli-errors/*": ["packages/kbn-dev-cli-errors/*"], "@kbn/dev-cli-runner": ["packages/kbn-dev-cli-runner"], @@ -822,6 +824,8 @@ "@kbn/entities-schema/*": ["x-pack/packages/kbn-entities-schema/*"], "@kbn/entity-manager-fixture-plugin": ["x-pack/test/api_integration/apis/entity_manager/fixture_plugin"], "@kbn/entity-manager-fixture-plugin/*": ["x-pack/test/api_integration/apis/entity_manager/fixture_plugin/*"], + "@kbn/entityManager-app-plugin": ["x-pack/plugins/observability_solution/entity_manager_app"], + "@kbn/entityManager-app-plugin/*": ["x-pack/plugins/observability_solution/entity_manager_app/*"], "@kbn/entityManager-plugin": ["x-pack/plugins/entity_manager"], "@kbn/entityManager-plugin/*": ["x-pack/plugins/entity_manager/*"], "@kbn/error-boundary-example-plugin": ["examples/error_boundary"], @@ -1530,6 +1534,8 @@ "@kbn/saved-objects-tagging-plugin/*": ["x-pack/plugins/saved_objects_tagging/*"], "@kbn/saved-search-plugin": ["src/plugins/saved_search"], "@kbn/saved-search-plugin/*": ["src/plugins/saved_search/*"], + "@kbn/scout": ["packages/kbn-scout"], + "@kbn/scout/*": ["packages/kbn-scout/*"], "@kbn/screenshot-mode-example-plugin": ["examples/screenshot_mode_example"], "@kbn/screenshot-mode-example-plugin/*": ["examples/screenshot_mode_example/*"], "@kbn/screenshot-mode-plugin": ["src/plugins/screenshot_mode"], @@ -1564,6 +1570,8 @@ "@kbn/search-indices/*": ["x-pack/plugins/search_indices/*"], "@kbn/search-inference-endpoints": ["x-pack/plugins/search_inference_endpoints"], "@kbn/search-inference-endpoints/*": ["x-pack/plugins/search_inference_endpoints/*"], + "@kbn/search-navigation": ["x-pack/plugins/search_solution/search_navigation"], + "@kbn/search-navigation/*": ["x-pack/plugins/search_solution/search_navigation/*"], "@kbn/search-notebooks": ["x-pack/plugins/search_notebooks"], "@kbn/search-notebooks/*": ["x-pack/plugins/search_notebooks/*"], "@kbn/search-playground": ["x-pack/plugins/search_playground"], @@ -1842,6 +1850,8 @@ "@kbn/stdio-dev-helpers/*": ["packages/kbn-stdio-dev-helpers/*"], "@kbn/storybook": ["packages/kbn-storybook"], "@kbn/storybook/*": ["packages/kbn-storybook/*"], + "@kbn/streams-app-plugin": ["x-pack/plugins/streams_app"], + "@kbn/streams-app-plugin/*": ["x-pack/plugins/streams_app/*"], "@kbn/streams-plugin": ["x-pack/plugins/streams"], "@kbn/streams-plugin/*": ["x-pack/plugins/streams/*"], "@kbn/synthetics-e2e": ["x-pack/plugins/observability_solution/synthetics/e2e"], diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 0e0e9aba84467..97efbef318c90 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -12,3 +12,4 @@ /.env /.kibana-plugin-helpers.dev.* .cache +**/ui_tests/output diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index e1e8478aa0517..213c3f06f34aa 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -112,7 +112,9 @@ "xpack.observabilityLogsOverview": [ "packages/observability/logs_overview/src/components" ], - "xpack.osquery": ["plugins/osquery"], + "xpack.osquery": [ + "plugins/osquery" + ], "xpack.painlessLab": "plugins/painless_lab", "xpack.profiling": [ "plugins/observability_solution/profiling" @@ -130,6 +132,7 @@ "xpack.searchSharedUI": "packages/search/shared_ui", "xpack.searchHomepage": "plugins/search_homepage", "xpack.searchIndices": "plugins/search_indices", + "xpack.searchNavigation": "plugins/search_solution/search_navigation", "xpack.searchNotebooks": "plugins/search_notebooks", "xpack.searchPlayground": "plugins/search_playground", "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", @@ -147,6 +150,9 @@ "xpack.securitySolutionEss": "plugins/security_solution_ess", "xpack.securitySolutionServerless": "plugins/security_solution_serverless", "xpack.sessionView": "plugins/session_view", + "xpack.streams": [ + "plugins/streams_app" + ], "xpack.slo": "plugins/observability_solution/slo", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": "plugins/spaces", diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index bebcb0aa88301..04f90dfbb96d4 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -23,7 +23,6 @@ import type { TypedLensByValueInput, PersistedIndexPatternLayer, XYState, - LensEmbeddableInput, FormulaPublicApi, DateHistogramIndexPatternColumn, } from '@kbn/lens-plugin/public'; @@ -288,7 +287,7 @@ export const App = (props: { /> {isSaveModalVisible && ( {}} onClose={() => setIsSaveModalVisible(false)} /> diff --git a/x-pack/examples/lens_embeddable_inline_editing_example/public/app.tsx b/x-pack/examples/lens_embeddable_inline_editing_example/public/app.tsx index 055050de3f4c6..68d7140badb30 100644 --- a/x-pack/examples/lens_embeddable_inline_editing_example/public/app.tsx +++ b/x-pack/examples/lens_embeddable_inline_editing_example/public/app.tsx @@ -24,7 +24,6 @@ import type { CoreStart } from '@kbn/core/public'; import { LensConfigBuilder } from '@kbn/lens-embeddable-utils/config_builder/config_builder'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { StartDependencies } from './plugin'; import { LensChart } from './embeddable'; import { MultiPaneFlyout } from './flyout'; @@ -46,137 +45,128 @@ export const App = (props: { ); return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + - -

#3: Embeddable inside a flyout

-
- - -

- In case you do not want to use a push flyout, you can check this example.{' '} -
- In this example, we have a Lens embeddable inside a flyout and we want to - render the inline editing Component in a second slot of the same flyout. -

-
- - - - { - setIsFlyoutVisible(true); - setPanelActive(3); +

#3: Embeddable inside a flyout

+
+ + +

+ In case you do not want to use a push flyout, you can check this example.
+ In this example, we have a Lens embeddable inside a flyout and we want to render + the inline editing Component in a second slot of the same flyout. +

+
+ + + + { + setIsFlyoutVisible(true); + setPanelActive(3); + }} + > + Show flyout + + {isFlyoutVisible ? ( + { + setIsinlineEditingVisible(false); + if (container) { + ReactDOM.unmountComponentAtNode(container); + } + }} + onCancelCb={() => { + setIsinlineEditingVisible(false); + if (container) { + ReactDOM.unmountComponentAtNode(container); + } + }} + isESQL + isActive + /> + ), + }} + inlineEditingContent={{ + visible: isInlineEditingVisible, + }} + setContainer={setContainer} + onClose={() => { + setIsFlyoutVisible(false); + setIsinlineEditingVisible(false); + setPanelActive(null); + if (container) { + ReactDOM.unmountComponentAtNode(container); + } }} - > - Show flyout - - {isFlyoutVisible ? ( - { - setIsinlineEditingVisible(false); - if (container) { - ReactDOM.unmountComponentAtNode(container); - } - }} - onCancelCb={() => { - setIsinlineEditingVisible(false); - if (container) { - ReactDOM.unmountComponentAtNode(container); - } - }} - isESQL - isActive - /> - ), - }} - inlineEditingContent={{ - visible: isInlineEditingVisible, - }} - setContainer={setContainer} - onClose={() => { - setIsFlyoutVisible(false); - setIsinlineEditingVisible(false); - setPanelActive(null); - if (container) { - ReactDOM.unmountComponentAtNode(container); - } - }} - /> - ) : null} - - -
-
-
-
-
-
-
+ /> + ) : null} + + + + + + + + ); }; diff --git a/x-pack/examples/lens_embeddable_inline_editing_example/public/embeddable.tsx b/x-pack/examples/lens_embeddable_inline_editing_example/public/embeddable.tsx index 717a8b2d20f8e..a63264485bf53 100644 --- a/x-pack/examples/lens_embeddable_inline_editing_example/public/embeddable.tsx +++ b/x-pack/examples/lens_embeddable_inline_editing_example/public/embeddable.tsx @@ -64,13 +64,13 @@ export const LensChart = (props: { ( isLoading: boolean, adapters: InlineEditLensEmbeddableContext['lensEvent']['adapters'] | undefined, - lensEmbeddableOutput$?: InlineEditLensEmbeddableContext['lensEvent']['embeddableOutput$'] + dataLoading$?: InlineEditLensEmbeddableContext['lensEvent']['dataLoading$'] ) => { const adapterTables = adapters?.tables?.tables; if (adapterTables && !isLoading) { setLensLoadEvent({ adapters, - embeddableOutput$: lensEmbeddableOutput$, + dataLoading$, }); } }, diff --git a/x-pack/examples/lens_embeddable_inline_editing_example/public/mount.tsx b/x-pack/examples/lens_embeddable_inline_editing_example/public/mount.tsx index 411538e2df2ca..86bf0757220d4 100644 --- a/x-pack/examples/lens_embeddable_inline_editing_example/public/mount.tsx +++ b/x-pack/examples/lens_embeddable_inline_editing_example/public/mount.tsx @@ -10,6 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EuiCallOut } from '@elastic/eui'; import type { CoreSetup, AppMountParameters } from '@kbn/core/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { StartDependencies } from './plugin'; export const mount = @@ -21,10 +22,15 @@ export const mount = const dataView = await plugins.dataViews.getDefaultDataView(); const stateHelpers = await plugins.lens.stateHelperApi(); - const i18nCore = core.i18n; - const reactElement = ( - + {dataView ? ( You need at least one dataview for this demo to work

)} -
+ ); render(reactElement, element); diff --git a/x-pack/examples/lens_embeddable_inline_editing_example/tsconfig.json b/x-pack/examples/lens_embeddable_inline_editing_example/tsconfig.json index e4727650106bd..104bfbeeacd7e 100644 --- a/x-pack/examples/lens_embeddable_inline_editing_example/tsconfig.json +++ b/x-pack/examples/lens_embeddable_inline_editing_example/tsconfig.json @@ -19,8 +19,8 @@ "@kbn/developer-examples-plugin", "@kbn/data-views-plugin", "@kbn/ui-actions-plugin", - "@kbn/kibana-react-plugin", "@kbn/lens-embeddable-utils", "@kbn/ui-theme", + "@kbn/react-kibana-context-render", ] } diff --git a/x-pack/examples/testing_embedded_lens/public/app.tsx b/x-pack/examples/testing_embedded_lens/public/app.tsx index 9aa6a40fe20cf..699db0d0dc644 100644 --- a/x-pack/examples/testing_embedded_lens/public/app.tsx +++ b/x-pack/examples/testing_embedded_lens/public/app.tsx @@ -29,7 +29,6 @@ import type { TypedLensByValueInput, PersistedIndexPatternLayer, XYState, - LensEmbeddableInput, DateHistogramIndexPatternColumn, DatatableVisualizationState, HeatmapVisualizationState, @@ -42,7 +41,6 @@ import type { MetricVisualizationState, } from '@kbn/lens-plugin/public'; import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { CodeEditor, HJsonLang } from '@kbn/code-editor'; import type { StartDependencies } from './plugin'; import { @@ -496,269 +494,256 @@ export const App = (props: { const [overrides, setOverrides] = useState(); return ( - - - - - - - - - - -

- This app embeds a Lens visualization by specifying the configuration. Data - fetching and rendering is completely managed by Lens itself. -

-

- The editor on the right hand side make it possible to paste a Lens - attributes configuration, and have it rendered. Presets are available to - have a starting configuration, and new presets can be saved as well (not - persisted). -

-

- The Open with Lens button will take the current configuration and navigate - to a prefilled editor. -

- - - - - - - - - - - + + + + + + + + + +

+ This app embeds a Lens visualization by specifying the configuration. Data + fetching and rendering is completely managed by Lens itself. +

+

+ The editor on the right hand side make it possible to paste a Lens attributes + configuration, and have it rendered. Presets are available to have a starting + configuration, and new presets can be saved as well (not persisted). +

+

+ The Open with Lens button will take the current configuration and navigate to + a prefilled editor. +

+ + + + + + + + + + + + + { + setIsSaveModalVisible(true); + }} + > + Save Visualization + + + {props.defaultDataView?.isTimeBased() ? ( { - setIsSaveModalVisible(true); - }} - > - Save Visualization - - - {props.defaultDataView?.isTimeBased() ? ( - - { - setTime( - time.to === 'now' - ? { - from: '2015-09-18T06:31:44.000Z', - to: '2015-09-23T18:31:44.000Z', - } - : { - from: 'now-5d', - to: 'now', - } - ); - }} - > - {time.to === 'now' ? 'Change time range' : 'Reset time range'} - - - ) : null} - - { - props.plugins.lens.navigateToPrefilledEditor( - { - id: '', - timeRange: time, - attributes: currentAttributes, - }, - { - openInNewTab: true, - } + setTime( + time.to === 'now' + ? { + from: '2015-09-18T06:31:44.000Z', + to: '2015-09-23T18:31:44.000Z', + } + : { + from: 'now-5d', + to: 'now', + } ); }} > - Open in Lens (new tab) + {time.to === 'now' ? 'Change time range' : 'Reset time range'} - -

State: {isLoading ? 'Loading...' : 'Rendered'}

-
-
- - - { - setIsLoading(val); - }} - onBrushEnd={({ range }) => { - setTime({ - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }); - }} - onFilter={(_data) => { - // call back event for on filter event - }} - onTableRowClick={(_data) => { - // call back event for on table row click event - }} - disableTriggers={!enableTriggers} - viewMode={ViewMode.VIEW} - withDefaultActions={enableDefaultAction} - extraActions={ - enableExtraAction - ? [ - { - id: 'testAction', - type: 'link', - getIconType: () => 'save', - async isCompatible( - context: ActionExecutionContext - ): Promise { - return true; - }, - execute: async (context: ActionExecutionContext) => { - alert('I am an extra action'); - return; - }, - getDisplayName: () => 'Extra action', - }, - ] - : undefined - } - /> - - - - {isSaveModalVisible && ( - {}} - onClose={() => setIsSaveModalVisible(false)} - /> - )} - - - - - - -

Paste or edit here your Lens document

-
-
-
- - - ({ value: i, text: id }))} - value={undefined} - onChange={(e) => switchChartPreset(+e.target.value)} - aria-label="Load from a preset" - prepend={'Load preset'} - /> - - - { - const attributes = checkAndParseSO(currentSO.current); - if (attributes) { - const label = `custom-chart-${chartCounter}`; - addChartConfiguration([ - ...loadedCharts, + ) : null} + + { + props.plugins.lens.navigateToPrefilledEditor( + { + id: '', + timeRange: time, + attributes: currentAttributes, + }, + { + openInNewTab: true, + } + ); + }} + > + Open in Lens (new tab) + + + +

State: {isLoading ? 'Loading...' : 'Rendered'}

+
+
+ + + { + setIsLoading(val); + }} + onBrushEnd={({ range }) => { + setTime({ + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }); + }} + onFilter={(_data) => { + // call back event for on filter event + }} + onTableRowClick={(_data) => { + // call back event for on table row click event + }} + disableTriggers={!enableTriggers} + viewMode={ViewMode.VIEW} + withDefaultActions={enableDefaultAction} + extraActions={ + enableExtraAction + ? [ { - id: label, - attributes, + id: 'testAction', + type: 'link', + getIconType: () => 'save', + async isCompatible( + context: ActionExecutionContext + ): Promise { + return true; + }, + execute: async (context: ActionExecutionContext) => { + alert('I am an extra action'); + return; + }, + getDisplayName: () => 'Extra action', }, - ]); - chartCounter++; - alert(`The preset has been saved as "${label}"`); - } - }} - > - Save as preset - - - {hasParsingErrorDebounced && currentSO.current !== currentValid && ( - -

Check the spec

-
- )} - - - - { - const isValid = Boolean(checkAndParseSO(newSO)); - setErrorFlag(!isValid); - currentSO.current = newSO; - if (isValid) { - // reset the debounced error - setErrorDebounced(isValid); - saveValidSO(newSO); - } - }} - /> - - - - - - - - - - - + ] + : undefined + } + /> + + + + {isSaveModalVisible && ( + {}} + onClose={() => setIsSaveModalVisible(false)} + /> + )} + + + + + + +

Paste or edit here your Lens document

+
+
+
+ + + ({ value: i, text: id }))} + value={undefined} + onChange={(e) => switchChartPreset(+e.target.value)} + aria-label="Load from a preset" + prepend={'Load preset'} + /> + + + { + const attributes = checkAndParseSO(currentSO.current); + if (attributes) { + const label = `custom-chart-${chartCounter}`; + addChartConfiguration([ + ...loadedCharts, + { + id: label, + attributes, + }, + ]); + chartCounter++; + alert(`The preset has been saved as "${label}"`); + } + }} + > + Save as preset + + + {hasParsingErrorDebounced && currentSO.current !== currentValid && ( + +

Check the spec

+
+ )} +
+ + + { + const isValid = Boolean(checkAndParseSO(newSO)); + setErrorFlag(!isValid); + currentSO.current = newSO; + if (isValid) { + // reset the debounced error + setErrorDebounced(isValid); + saveValidSO(newSO); + } + }} + /> + + +
+
+ + + + + + ); }; diff --git a/x-pack/examples/testing_embedded_lens/public/mount.tsx b/x-pack/examples/testing_embedded_lens/public/mount.tsx index d0f58eb6050b7..04099e125b968 100644 --- a/x-pack/examples/testing_embedded_lens/public/mount.tsx +++ b/x-pack/examples/testing_embedded_lens/public/mount.tsx @@ -11,6 +11,7 @@ import { EuiCallOut } from '@elastic/eui'; import type { CoreSetup, AppMountParameters } from '@kbn/core/public'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { StartDependencies } from './plugin'; export const mount = @@ -24,10 +25,15 @@ export const mount = const dataView = await plugins.data.indexPatterns.getDefault(); const stateHelpers = await plugins.lens.stateHelperApi(); - const i18nCore = core.i18n; - const reactElement = ( - + {dataView ? ( This demo only works if your default index pattern is set and time based

)} -
+ ); render(reactElement, element); diff --git a/x-pack/examples/testing_embedded_lens/tsconfig.json b/x-pack/examples/testing_embedded_lens/tsconfig.json index 90cf691a3529c..efa0ebd803d93 100644 --- a/x-pack/examples/testing_embedded_lens/tsconfig.json +++ b/x-pack/examples/testing_embedded_lens/tsconfig.json @@ -21,8 +21,8 @@ "@kbn/developer-examples-plugin", "@kbn/data-views-plugin", "@kbn/ui-actions-plugin", - "@kbn/kibana-react-plugin", "@kbn/core-ui-settings-browser", "@kbn/code-editor", + "@kbn/react-kibana-context-render", ] } diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index 7bfe505face19..5df10e11bb7ed 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -23,6 +23,13 @@ export interface MetadataRecord { [key: string]: string[] | MetadataRecord | string; } +export interface EntityV2 { + 'entity.id': string; + 'entity.last_seen_timestamp': string; + 'entity.type': string; + [metadata: string]: any; +} + const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_overview.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_overview.ts index 9983bdee41e2d..679abc3d7f96a 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_overview.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_overview.ts @@ -18,10 +18,6 @@ const getOverviewResponseSchema = t.type({ degrading: t.number, stale: t.number, healthy: t.number, - worst: t.type({ - value: t.number, - id: t.string, - }), noData: t.number, burnRateRules: t.number, burnRateActiveAlerts: t.number, diff --git a/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.test.ts b/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.test.ts index 22cee17bb1a64..aa25821b4ac14 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.test.ts +++ b/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.test.ts @@ -37,4 +37,16 @@ describe('unflattenObject', () => { }, }); }); + + it('handles null values correctly', () => { + expect( + unflattenObject({ + 'agent.name': null, + }) + ).toEqual({ + agent: { + name: null, + }, + }); + }); }); diff --git a/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.ts b/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.ts index 83508d5d2dbf5..8a4493905f1d4 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.ts +++ b/x-pack/packages/observability/observability_utils/observability_utils_common/object/unflatten_object.ts @@ -6,13 +6,17 @@ */ import { set } from '@kbn/safer-lodash-set'; +import { DedotObject } from '@kbn/utility-types'; -export function unflattenObject(source: Record, target: Record = {}) { +export function unflattenObject>( + source: T, + target: Record = {} +): DedotObject { // eslint-disable-next-line guard-for-in for (const key in source) { const val = source[key as keyof typeof source]; if (Array.isArray(val)) { - const unflattenedArray = val.map((item) => { + const unflattenedArray = val.map((item: unknown) => { if (item && typeof item === 'object' && !Array.isArray(item)) { return unflattenObject(item); } @@ -23,5 +27,6 @@ export function unflattenObject(source: Record, target: Record; } diff --git a/x-pack/packages/observability/observability_utils/observability_utils_common/tsconfig.json b/x-pack/packages/observability/observability_utils/observability_utils_common/tsconfig.json index 7954cdc946e9c..e2226268918a7 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_common/tsconfig.json +++ b/x-pack/packages/observability/observability_utils/observability_utils_common/tsconfig.json @@ -19,5 +19,6 @@ "@kbn/es-query", "@kbn/safer-lodash-set", "@kbn/inference-common", + "@kbn/utility-types", ] } diff --git a/x-pack/packages/observability/observability_utils/observability_utils_server/es/client/create_observability_es_client.ts b/x-pack/packages/observability/observability_utils/observability_utils_server/es/client/create_observability_es_client.ts index 78ed20a582bc3..92c7b8d19e531 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_server/es/client/create_observability_es_client.ts +++ b/x-pack/packages/observability/observability_utils/observability_utils_server/es/client/create_observability_es_client.ts @@ -10,12 +10,15 @@ import type { FieldCapsRequest, FieldCapsResponse, MsearchRequest, + ScalarValue, SearchResponse, } from '@elastic/elasticsearch/lib/api/types'; import { withSpan } from '@kbn/apm-utils'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; -import { Required } from 'utility-types'; +import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { Required, ValuesType } from 'utility-types'; +import { DedotObject } from '@kbn/utility-types'; +import { unflattenObject } from '@kbn/task-manager-plugin/server/metrics/lib'; import { esqlResultToPlainObjects } from '../esql_result_to_plain_objects'; type SearchRequest = ESSearchRequest & { @@ -24,19 +27,52 @@ type SearchRequest = ESSearchRequest & { size: number | boolean; }; -type EsqlQueryParameters = EsqlQueryRequest & { parseOutput?: boolean }; -type EsqlOutputParameters = Omit & { - parseOutput?: true; - format?: 'json'; - columnar?: false; -}; +export interface EsqlOptions { + transform?: 'none' | 'plain' | 'unflatten'; +} + +export type EsqlValue = ScalarValue | ScalarValue[]; + +export type EsqlOutput = Record; + +type MaybeUnflatten, TApply> = TApply extends true + ? DedotObject + : T; + +interface UnparsedEsqlResponseOf { + columns: Array<{ name: keyof TOutput; type: string }>; + values: Array>>; +} -type EsqlParameters = EsqlOutputParameters | EsqlQueryParameters; +interface ParsedEsqlResponseOf< + TOutput extends EsqlOutput, + TOptions extends EsqlOptions | undefined = { transform: 'none' } +> { + hits: Array< + MaybeUnflatten< + { + [key in keyof TOutput]: TOutput[key]; + }, + TOptions extends { transform: 'unflatten' } ? true : false + > + >; +} export type InferEsqlResponseOf< - TOutput = unknown, - TParameters extends EsqlParameters = EsqlParameters -> = TParameters extends EsqlOutputParameters ? TOutput[] : ESQLSearchResponse; + TOutput extends EsqlOutput, + TOptions extends EsqlOptions | undefined = { transform: 'none' } +> = TOptions extends { transform: 'plain' | 'unflatten' } + ? ParsedEsqlResponseOf + : UnparsedEsqlResponseOf; + +export type ObservabilityESSearchRequest = SearchRequest; + +export type ObservabilityEsQueryRequest = Omit; + +export type ParsedEsqlResponse = ParsedEsqlResponseOf; +export type UnparsedEsqlResponse = UnparsedEsqlResponseOf; + +export type EsqlQueryResponse = UnparsedEsqlResponse | ParsedEsqlResponse; /** * An Elasticsearch Client with a fully typed `search` method and built-in @@ -57,14 +93,18 @@ export interface ObservabilityElasticsearchClient { operationName: string, request: Required ): Promise; - esql( + esql( operationName: string, - parameters: TQueryParams - ): Promise>; - esql( + parameters: ObservabilityEsQueryRequest + ): Promise>; + esql< + TOutput extends EsqlOutput = EsqlOutput, + TEsqlOptions extends EsqlOptions = { transform: 'none' } + >( operationName: string, - parameters: TQueryParams - ): Promise>; + parameters: ObservabilityEsQueryRequest, + options: TEsqlOptions + ): Promise>; client: ElasticsearchClient; } @@ -109,32 +149,41 @@ export function createObservabilityEsClient({ }); }); }, - esql( + esql( operationName: string, - { parseOutput = true, format = 'json', columnar = false, ...parameters }: TSearchRequest - ) { - logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`); - return withSpan({ name: operationName, labels: { plugin } }, () => { - return client.esql.query( - { ...parameters, format, columnar }, - { - querystring: { - drop_null_columns: true, - }, - } - ); - }) - .then((response) => { - logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`); - - const esqlResponse = response as unknown as ESQLSearchResponse; - - const shouldParseOutput = parseOutput && !columnar && format === 'json'; - return shouldParseOutput ? esqlResultToPlainObjects(esqlResponse) : esqlResponse; - }) - .catch((error) => { - throw error; - }); + parameters: ObservabilityEsQueryRequest, + options?: EsqlOptions + ): Promise> { + return callWithLogger(operationName, parameters, () => { + return client.esql + .query( + { ...parameters }, + { + querystring: { + drop_null_columns: true, + }, + } + ) + .then((response) => { + const esqlResponse = response as unknown as UnparsedEsqlResponseOf; + + const transform = options?.transform ?? 'none'; + + if (transform === 'none') { + return esqlResponse; + } + + const parsedResponse = { hits: esqlResultToPlainObjects(esqlResponse) }; + + if (transform === 'plain') { + return parsedResponse; + } + + return { + hits: parsedResponse.hits.map((hit) => unflattenObject(hit)), + }; + }) as Promise>; + }); }, search( operationName: string, diff --git a/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.test.ts b/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.test.ts index 4557d0ba0bdd5..55d77368cfdfb 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.test.ts +++ b/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.test.ts @@ -27,40 +27,15 @@ describe('esqlResultToPlainObjects', () => { expect(output).toEqual([{ name: 'Foo Bar' }]); }); - it('should return columns without "text" or "keyword" in their names', () => { + it('should not unflatten objects', () => { const result: ESQLSearchResponse = { columns: [ - { name: 'name.text', type: 'text' }, - { name: 'age', type: 'keyword' }, - ], - values: [ - ['Foo Bar', 30], - ['Foo Qux', 25], - ], - }; - const output = esqlResultToPlainObjects(result); - expect(output).toEqual([ - { name: 'Foo Bar', age: 30 }, - { name: 'Foo Qux', age: 25 }, - ]); - }); - - it('should handle mixed columns correctly', () => { - const result: ESQLSearchResponse = { - columns: [ - { name: 'name', type: 'text' }, - { name: 'name.text', type: 'text' }, - { name: 'age', type: 'keyword' }, - ], - values: [ - ['Foo Bar', 'Foo Bar', 30], - ['Foo Qux', 'Foo Qux', 25], + { name: 'name', type: 'keyword' }, + { name: 'name.nested', type: 'keyword' }, ], + values: [['Foo Bar', 'Bar Foo']], }; const output = esqlResultToPlainObjects(result); - expect(output).toEqual([ - { name: 'Foo Bar', age: 30 }, - { name: 'Foo Qux', age: 25 }, - ]); + expect(output).toEqual([{ name: 'Foo Bar', 'name.nested': 'Bar Foo' }]); }); }); diff --git a/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.ts index 34781153532c5..53f54b608ca35 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/observability_utils_server/es/esql_result_to_plain_objects.ts @@ -6,28 +6,24 @@ */ import type { ESQLSearchResponse } from '@kbn/es-types'; -import { unflattenObject } from '@kbn/observability-utils-common/object/unflatten_object'; -export function esqlResultToPlainObjects( - result: ESQLSearchResponse -): TDocument[] { - return result.values.map((row) => { - return unflattenObject( - row.reduce>((acc, value, index) => { - const column = result.columns[index]; +export function esqlResultToPlainObjects< + TDocument extends Record = Record +>(result: ESQLSearchResponse): TDocument[] { + return result.values.map((row): TDocument => { + return row.reduce>((acc, value, index) => { + const column = result.columns[index]; - if (!column) { - return acc; - } + if (!column) { + return acc; + } - // Removes the type suffix from the column name - const name = column.name.replace(/\.(text|keyword)$/, ''); - if (!acc[name]) { - acc[name] = value; - } + const name = column.name; + if (!acc[name]) { + acc[name] = value; + } - return acc; - }, {}) - ) as TDocument; + return acc; + }, {}) as TDocument; }); } diff --git a/x-pack/packages/observability/observability_utils/observability_utils_server/tsconfig.json b/x-pack/packages/observability/observability_utils/observability_utils_server/tsconfig.json index f51d93089c627..f6dd781184b86 100644 --- a/x-pack/packages/observability/observability_utils/observability_utils_server/tsconfig.json +++ b/x-pack/packages/observability/observability_utils/observability_utils_server/tsconfig.json @@ -24,5 +24,7 @@ "@kbn/alerting-plugin", "@kbn/rule-registry-plugin", "@kbn/rule-data-utils", + "@kbn/utility-types", + "@kbn/task-manager-plugin", ] } diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 57304c176c13d..edfd7606950c1 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -140,6 +140,7 @@ export interface PluginSetupContract { getActionsHealth: () => { hasPermanentEncryptionKey: boolean }; getActionsConfigurationUtilities: () => ActionsConfigurationUtilities; setEnabledConnectorTypes: (connectorTypes: EnabledConnectorTypes) => void; + isActionTypeEnabled(id: string, options?: { notifyUsage: boolean }): boolean; } @@ -168,6 +169,7 @@ export interface PluginStartContract { params: Params, variables: Record ): Params; + isSystemActionConnector: (connectorId: string) => boolean; } diff --git a/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.test.ts b/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.test.ts index 77dec7f15e156..2c09576cc2a2b 100644 --- a/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.test.ts +++ b/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.test.ts @@ -230,10 +230,11 @@ describe('ConnectorUsageReportingTask', () => { const response = await taskRunner.run(); expect(logger.warn).toHaveBeenCalledWith( - 'Missing required project id while running actions:connector_usage_reporting' + 'Missing required project id while running actions:connector_usage_reporting, reporting task will be deleted' ); expect(response).toEqual({ + shouldDeleteTask: true, state: { attempts: 0, lastReportedUsageDate, @@ -391,4 +392,27 @@ describe('ConnectorUsageReportingTask', () => { 'Usage data could not be pushed to usage-api. Stopped retrying after 5 attempts. Error:test-error' ); }); + + it('does not schedule the task when the project id is missing', async () => { + const core = createSetup(); + const taskManagerStart = taskManagerStartMock(); + + const task = new ConnectorUsageReportingTask({ + eventLogIndex: 'test-index', + projectId: undefined, + logger, + core, + taskManager: mockTaskManagerSetup, + config: { + url: 'usage-api', + ca: { + path: './ca.crt', + }, + }, + }); + + await task.start(taskManagerStart); + + expect(taskManagerStart.ensureScheduled).not.toBeCalled(); + }); }); diff --git a/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.ts b/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.ts index ce44718749006..ddaa930b15c34 100644 --- a/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.ts +++ b/x-pack/plugins/actions/server/usage/connector_usage_reporting_task.ts @@ -82,6 +82,9 @@ export class ConnectorUsageReportingTask { } public start = async (taskManager?: TaskManagerStartContract) => { + if (!this.projectId) { + return; + } if (!taskManager) { this.logger.error( `Missing required task manager service during start of ${CONNECTOR_USAGE_REPORTING_TASK_TYPE}` @@ -111,10 +114,11 @@ export class ConnectorUsageReportingTask { if (!this.projectId) { this.logger.warn( - `Missing required project id while running ${CONNECTOR_USAGE_REPORTING_TASK_TYPE}` + `Missing required project id while running ${CONNECTOR_USAGE_REPORTING_TASK_TYPE}, reporting task will be deleted` ); return { state, + shouldDeleteTask: true, }; } diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts b/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts index dbede9f7d94d3..1e6b5545ebb4e 100644 --- a/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts +++ b/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts @@ -29,10 +29,10 @@ export const registerInstallationRoutes = ({ validate: false, options: { access: 'internal', - security: { - authz: { - requiredPrivileges: ['manage_llm_product_doc'], - }, + }, + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], }, }, }, @@ -56,13 +56,13 @@ export const registerInstallationRoutes = ({ validate: false, options: { access: 'internal', - security: { - authz: { - requiredPrivileges: ['manage_llm_product_doc'], - }, - }, timeout: { idleSocket: 20 * 60 * 1000 }, // install can take time. }, + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], + }, + }, }, async (ctx, req, res) => { const { documentationManager } = getServices(); @@ -90,10 +90,10 @@ export const registerInstallationRoutes = ({ validate: false, options: { access: 'internal', - security: { - authz: { - requiredPrivileges: ['manage_llm_product_doc'], - }, + }, + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], }, }, }, diff --git a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx index e6f58df8d3a7f..367da7e65811a 100644 --- a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook, act } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useArchiveMaintenanceWindow } from './use_archive_maintenance_window'; diff --git a/x-pack/plugins/alerting/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/alerting/public/hooks/use_breadcrumbs.test.tsx index 9eb5970e86a9f..f06fd2be67996 100644 --- a/x-pack/plugins/alerting/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_breadcrumbs.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useBreadcrumbs } from './use_breadcrumbs'; import { MAINTENANCE_WINDOW_DEEP_LINK_IDS } from '../../common'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; diff --git a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.test.tsx index 26d70f2d4e9a8..12564df1bf1b4 100644 --- a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook, act } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useCreateMaintenanceWindow } from './use_create_maintenance_window'; diff --git a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.test.tsx b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.test.tsx index d21b145aea937..b543d7940cd9d 100644 --- a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useFindMaintenanceWindows } from './use_find_maintenance_windows'; diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx index 8b55812bd0301..7e453d5d78d59 100644 --- a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook, act } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useFinishAndArchiveMaintenanceWindow } from './use_finish_and_archive_maintenance_window'; diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx index 6041796fcc00c..fc972eddeafee 100644 --- a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook, act } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useFinishMaintenanceWindow } from './use_finish_maintenance_window'; diff --git a/x-pack/plugins/alerting/public/hooks/use_get_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_get_maintenance_window.test.tsx index 3003f1003ce12..d58aebe0a1578 100644 --- a/x-pack/plugins/alerting/public/hooks/use_get_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_get_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useGetMaintenanceWindow } from './use_get_maintenance_window'; diff --git a/x-pack/plugins/alerting/public/hooks/use_license.test.tsx b/x-pack/plugins/alerting/public/hooks/use_license.test.tsx index 0611a6ba86aec..f9d4396072574 100644 --- a/x-pack/plugins/alerting/public/hooks/use_license.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_license.test.tsx @@ -6,7 +6,7 @@ */ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLicense } from './use_license'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; diff --git a/x-pack/plugins/alerting/public/hooks/use_navigation.test.tsx b/x-pack/plugins/alerting/public/hooks/use_navigation.test.tsx index 2ea981db2a4d5..1b7c48ee684e9 100644 --- a/x-pack/plugins/alerting/public/hooks/use_navigation.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_navigation.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useCreateMaintenanceWindowNavigation, diff --git a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx index 6ba19c27c362e..a1da94422c898 100644 --- a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; + +import { waitFor, renderHook, act } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; import { useUpdateMaintenanceWindow } from './use_update_maintenance_window'; diff --git a/x-pack/plugins/canvas/public/components/hooks/use_canvas_api.tsx b/x-pack/plugins/canvas/public/components/hooks/use_canvas_api.tsx index fa302c57ead8c..d815864fb4a60 100644 --- a/x-pack/plugins/canvas/public/components/hooks/use_canvas_api.tsx +++ b/x-pack/plugins/canvas/public/components/hooks/use_canvas_api.tsx @@ -49,6 +49,8 @@ export const useCanvasApi: () => CanvasContainerApi = () => { createNewEmbeddable(panelType, initialState); }, disableTriggers: true, + // this is required to disable inline editing now enabled by default + canEditInline: false, type: 'canvas', /** * getSerializedStateForChild is left out here because we cannot access the state here. That method diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 8e3b2644ee01a..916b49163b696 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -31,7 +31,6 @@ export type { CaseViewRefreshPropInterface, CasesPermissions, CasesCapabilities, - CasesStatus, } from './ui/types'; export { CaseSeverity } from './types/domain'; diff --git a/x-pack/plugins/cases/common/types/api/metrics/v1.ts b/x-pack/plugins/cases/common/types/api/metrics/v1.ts index 895c0fe99b3bf..43302cfb3bd80 100644 --- a/x-pack/plugins/cases/common/types/api/metrics/v1.ts +++ b/x-pack/plugins/cases/common/types/api/metrics/v1.ts @@ -23,6 +23,7 @@ export enum CaseMetricsFeature { CONNECTORS = 'connectors', LIFESPAN = 'lifespan', MTTR = 'mttr', + STATUS = 'status', } export const SingleCaseMetricsFeatureFieldRt = rt.union([ @@ -37,6 +38,7 @@ export const SingleCaseMetricsFeatureFieldRt = rt.union([ export const CasesMetricsFeatureFieldRt = rt.union([ SingleCaseMetricsFeatureFieldRt, rt.literal(CaseMetricsFeature.MTTR), + rt.literal(CaseMetricsFeature.STATUS), ]); const StatusInfoRt = rt.strict({ @@ -210,6 +212,10 @@ export const CasesMetricsResponseRt = rt.exact( * The average resolve time of all cases in seconds */ mttr: rt.union([rt.number, rt.null]), + /** + * The number of total cases per status + */ + status: rt.strict({ open: rt.number, inProgress: rt.number, closed: rt.number }), }) ); diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 99c92e0dbb55b..a03f38979ceac 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -37,7 +37,6 @@ import type { import type { CasePatchRequest, CasesFindResponse, - CasesStatusResponse, CaseUserActionStatsResponse, GetCaseConnectorsResponse, GetCaseUsersResponse, @@ -105,7 +104,6 @@ export type CasesUI = CaseUI[]; export type CasesFindResponseUI = Omit, 'cases'> & { cases: CasesUI; }; -export type CasesStatus = SnakeToCamelCase; export type CasesMetrics = SnakeToCamelCase; export type CaseUpdateRequest = SnakeToCamelCase; export type CaseConnectors = SnakeToCamelCase; diff --git a/x-pack/plugins/cases/public/api/__mocks__/index.ts b/x-pack/plugins/cases/public/api/__mocks__/index.ts index ddf651b008d49..9b9d1e90d2c30 100644 --- a/x-pack/plugins/cases/public/api/__mocks__/index.ts +++ b/x-pack/plugins/cases/public/api/__mocks__/index.ts @@ -6,15 +6,9 @@ */ import type { HTTPService } from '..'; -import { casesMetrics, casesStatus } from '../../containers/mock'; -import type { CasesMetrics, CasesStatus } from '../../containers/types'; -import type { CasesFindRequest, CasesMetricsRequest } from '../../../common/types/api'; - -export const getCasesStatus = async ({ - http, - signal, - query, -}: HTTPService & { query: CasesFindRequest }): Promise => Promise.resolve(casesStatus); +import { casesMetrics } from '../../containers/mock'; +import type { CasesMetrics } from '../../containers/types'; +import type { CasesMetricsRequest } from '../../../common/types/api'; export const getCasesMetrics = async ({ http, diff --git a/x-pack/plugins/cases/public/api/decoders.ts b/x-pack/plugins/cases/public/api/decoders.ts index 57c5f03a1790c..c2f9f466ec69d 100644 --- a/x-pack/plugins/cases/public/api/decoders.ts +++ b/x-pack/plugins/cases/public/api/decoders.ts @@ -11,13 +11,11 @@ import { pipe } from 'fp-ts/lib/pipeable'; import type { CasesFindResponse, - CasesStatusResponse, CasesBulkGetResponse, CasesMetricsResponse, } from '../../common/types/api'; import { CasesFindResponseRt, - CasesStatusResponseRt, CasesBulkGetResponseRt, CasesMetricsResponseRt, } from '../../common/types/api'; @@ -26,13 +24,6 @@ import { throwErrors } from '../../common'; export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => - pipe( - CasesStatusResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - export const decodeCasesMetricsResponse = (metrics?: CasesMetricsResponse) => pipe( CasesMetricsResponseRt.decode(metrics), diff --git a/x-pack/plugins/cases/public/api/index.ts b/x-pack/plugins/cases/public/api/index.ts index fbdf3c0bf6bc7..86fc266224acf 100644 --- a/x-pack/plugins/cases/public/api/index.ts +++ b/x-pack/plugins/cases/public/api/index.ts @@ -9,18 +9,15 @@ import type { HttpStart } from '@kbn/core/public'; import type { CasesFindRequest, CasesFindResponse, - CasesStatusRequest, - CasesStatusResponse, CasesBulkGetRequest, CasesBulkGetResponse, CasesMetricsRequest, CasesMetricsResponse, } from '../../common/types/api'; -import type { CasesStatus, CasesMetrics, CasesFindResponseUI } from '../../common/ui'; +import type { CasesMetrics, CasesFindResponseUI } from '../../common/ui'; import { CASE_FIND_URL, INTERNAL_CASE_METRICS_URL, - CASE_STATUS_URL, INTERNAL_BULK_GET_CASES_URL, } from '../../common/constants'; import { convertAllCasesToCamel, convertToCamelCase } from './utils'; @@ -28,7 +25,6 @@ import { decodeCasesBulkGetResponse, decodeCasesFindResponse, decodeCasesMetricsResponse, - decodeCasesStatusResponse, } from './decoders'; export interface HTTPService { @@ -45,19 +41,6 @@ export const getCases = async ({ return convertAllCasesToCamel(decodeCasesFindResponse(res)); }; -export const getCasesStatus = async ({ - http, - query, - signal, -}: HTTPService & { query: CasesStatusRequest }): Promise => { - const response = await http.get(CASE_STATUS_URL, { - signal, - query, - }); - - return convertToCamelCase(decodeCasesStatusResponse(response)); -}; - export const getCasesMetrics = async ({ http, signal, diff --git a/x-pack/plugins/cases/public/client/api/index.ts b/x-pack/plugins/cases/public/client/api/index.ts index 822d8cea27377..214c5d80f39e5 100644 --- a/x-pack/plugins/cases/public/client/api/index.ts +++ b/x-pack/plugins/cases/public/client/api/index.ts @@ -10,12 +10,11 @@ import type { CasesByAlertIDRequest, GetRelatedCasesByAlertResponse, CasesFindRequest, - CasesStatusRequest, CasesMetricsRequest, } from '../../../common/types/api'; import { getCasesFromAlertsUrl } from '../../../common/api'; -import { bulkGetCases, getCases, getCasesMetrics, getCasesStatus } from '../../api'; -import type { CasesFindResponseUI, CasesStatus, CasesMetrics } from '../../../common/ui'; +import { bulkGetCases, getCases, getCasesMetrics } from '../../api'; +import type { CasesFindResponseUI, CasesMetrics } from '../../../common/ui'; import type { CasesPublicStart } from '../../types'; export const createClientAPI = ({ http }: { http: HttpStart }): CasesPublicStart['api'] => { @@ -28,8 +27,6 @@ export const createClientAPI = ({ http }: { http: HttpStart }): CasesPublicStart cases: { find: (query: CasesFindRequest, signal?: AbortSignal): Promise => getCases({ http, query, signal }), - getCasesStatus: (query: CasesStatusRequest, signal?: AbortSignal): Promise => - getCasesStatus({ http, query, signal }), getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal): Promise => getCasesMetrics({ http, signal, query }), bulkGet: (params, signal?: AbortSignal) => bulkGetCases({ http, signal, params }), diff --git a/x-pack/plugins/cases/public/common/apm/use_cases_transactions.test.ts b/x-pack/plugins/cases/public/common/apm/use_cases_transactions.test.ts index c1cf6b305d48d..9a5709c911fbc 100644 --- a/x-pack/plugins/cases/public/common/apm/use_cases_transactions.test.ts +++ b/x-pack/plugins/cases/public/common/apm/use_cases_transactions.test.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { CaseAttachmentsWithoutOwner } from '../../types'; -import type { - StartAddAttachmentToExistingCaseTransaction, - StartCreateCaseWithAttachmentsTransaction, -} from './use_cases_transactions'; import { useAddAttachmentToExistingCaseTransaction, useCreateCaseWithAttachmentsTransaction, @@ -37,14 +33,10 @@ const bulkAttachments = [ ] as CaseAttachmentsWithoutOwner; const renderUseCreateCaseWithAttachmentsTransaction = () => - renderHook( - useCreateCaseWithAttachmentsTransaction - ); + renderHook(useCreateCaseWithAttachmentsTransaction); const renderUseAddAttachmentToExistingCaseTransaction = () => - renderHook( - useAddAttachmentToExistingCaseTransaction - ); + renderHook(useAddAttachmentToExistingCaseTransaction); describe('cases transactions', () => { beforeEach(() => { diff --git a/x-pack/plugins/cases/public/common/hooks.test.tsx b/x-pack/plugins/cases/public/common/hooks.test.tsx index d2cea878504bb..85dcfe11aaf5c 100644 --- a/x-pack/plugins/cases/public/common/hooks.test.tsx +++ b/x-pack/plugins/cases/public/common/hooks.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { TestProviders } from './mock'; import { useIsMainApplication } from './hooks'; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx index 60b798d37822a..73d1822c62499 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useApplicationCapabilities } from './hooks'; import { allCasesPermissions, TestProviders } from '../../mock'; @@ -14,10 +14,7 @@ import { allCasesPermissions, TestProviders } from '../../mock'; describe('hooks', () => { describe('useApplicationCapabilities', () => { it('should return the correct capabilities', async () => { - const { result } = renderHook< - React.PropsWithChildren<{}>, - ReturnType - >(() => useApplicationCapabilities(), { + const { result } = renderHook(() => useApplicationCapabilities(), { wrapper: ({ children }) => {children}, }); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/use_application.test.tsx b/x-pack/plugins/cases/public/common/lib/kibana/use_application.test.tsx index 81152d3d3c5a1..69c5fc4f8db2e 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/use_application.test.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/use_application.test.tsx @@ -7,7 +7,7 @@ import type { PublicAppInfo } from '@kbn/core-application-browser'; import { AppStatus } from '@kbn/core-application-browser'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { BehaviorSubject, Subject } from 'rxjs'; import type { AppMockRenderer } from '../../mock'; import { createAppMockRenderer } from '../../mock'; diff --git a/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx b/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx index 2170ed2d0e583..867e4d682695d 100644 --- a/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx +++ b/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; import { APP_ID } from '../../../common/constants'; import { useNavigation } from '../lib/kibana'; diff --git a/x-pack/plugins/cases/public/common/use_cases_features.test.tsx b/x-pack/plugins/cases/public/common/use_cases_features.test.tsx index eeabf3fb0cab2..c4c54af0b1c42 100644 --- a/x-pack/plugins/cases/public/common/use_cases_features.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_features.test.tsx @@ -6,10 +6,9 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import type { CasesContextFeatures } from '../../common/ui'; -import type { UseCasesFeatures } from './use_cases_features'; import { useCasesFeatures } from './use_cases_features'; import { TestProviders } from './mock/test_providers'; import type { LicenseType } from '@kbn/licensing-plugin/common/types'; @@ -37,14 +36,9 @@ describe('useCasesFeatures', () => { it.each(tests)( 'returns isAlertsEnabled=%s and isSyncAlertsEnabled=%s if feature.alerts=%s', async (isAlertsEnabled, isSyncAlertsEnabled, alerts) => { - const { result } = renderHook, UseCasesFeatures>( - () => useCasesFeatures(), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + const { result } = renderHook(() => useCasesFeatures(), { + wrapper: ({ children }) => {children}, + }); expect(result.current).toEqual({ isAlertsEnabled, @@ -57,16 +51,13 @@ describe('useCasesFeatures', () => { ); it('returns the metrics correctly', async () => { - const { result } = renderHook, UseCasesFeatures>( - () => useCasesFeatures(), - { - wrapper: ({ children }) => ( - - {children} - - ), - } - ); + const { result } = renderHook(() => useCasesFeatures(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); expect(result.current).toEqual({ isAlertsEnabled: true, @@ -91,12 +82,9 @@ describe('useCasesFeatures', () => { license: { type }, }); - const { result } = renderHook, UseCasesFeatures>( - () => useCasesFeatures(), - { - wrapper: ({ children }) => {children}, - } - ); + const { result } = renderHook(() => useCasesFeatures(), { + wrapper: ({ children }) => {children}, + }); expect(result.current).toEqual({ isAlertsEnabled: true, diff --git a/x-pack/plugins/cases/public/common/use_cases_local_storage.test.tsx b/x-pack/plugins/cases/public/common/use_cases_local_storage.test.tsx index 740fa78dc3c0d..92dd5808b6b7a 100644 --- a/x-pack/plugins/cases/public/common/use_cases_local_storage.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_local_storage.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { Subject } from 'rxjs'; import type { AppMockRenderer } from './mock/test_providers'; import { createAppMockRenderer } from './mock/test_providers'; diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index bb0c0b3a9f53c..c1f7d67a8b8b1 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import { useKibana, useToasts } from './lib/kibana'; import type { AppMockRenderer } from './mock'; import { createAppMockRenderer, TestProviders } from './mock'; @@ -14,7 +13,7 @@ import { alertComment, basicComment, mockCase } from '../containers/mock'; import React from 'react'; import userEvent from '@testing-library/user-event'; import type { SupportedCaseAttachment } from '../types'; -import { getByTestId, queryByTestId, screen } from '@testing-library/react'; +import { getByTestId, queryByTestId, screen, renderHook } from '@testing-library/react'; import { OWNER_INFO } from '../../common/constants'; import { useApplication } from './lib/kibana/use_application'; diff --git a/x-pack/plugins/cases/public/common/use_is_user_typing.test.tsx b/x-pack/plugins/cases/public/common/use_is_user_typing.test.tsx index 229ecc13bba0d..d4ec973469960 100644 --- a/x-pack/plugins/cases/public/common/use_is_user_typing.test.tsx +++ b/x-pack/plugins/cases/public/common/use_is_user_typing.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import type { AppMockRenderer } from './mock'; import { createAppMockRenderer } from './mock'; import { useIsUserTyping } from './use_is_user_typing'; diff --git a/x-pack/plugins/cases/public/common/use_license.test.tsx b/x-pack/plugins/cases/public/common/use_license.test.tsx index 0c28be2ca746d..8a5c29394cc62 100644 --- a/x-pack/plugins/cases/public/common/use_license.test.tsx +++ b/x-pack/plugins/cases/public/common/use_license.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { TestProviders } from './mock'; import { useLicense } from './use_license'; diff --git a/x-pack/plugins/cases/public/components/actions/assignees/use_assignees_action.test.tsx b/x-pack/plugins/cases/public/components/actions/assignees/use_assignees_action.test.tsx index 98cac1dfaf466..78b7801b699f8 100644 --- a/x-pack/plugins/cases/public/components/actions/assignees/use_assignees_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/assignees/use_assignees_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useAssigneesAction } from './use_assignees_action'; import * as api from '../../../containers/api'; @@ -56,7 +56,7 @@ describe('useAssigneesAction', () => { it('update the assignees correctly', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAssigneesAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -92,7 +92,7 @@ describe('useAssigneesAction', () => { }); it('shows the success toaster correctly when updating one case', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAssigneesAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -118,7 +118,7 @@ describe('useAssigneesAction', () => { }); it('shows the success toaster correctly when updating multiple cases', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useAssigneesAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, diff --git a/x-pack/plugins/cases/public/components/actions/copy_id/use_copy_id_action.test.tsx b/x-pack/plugins/cases/public/components/actions/copy_id/use_copy_id_action.test.tsx index 388b3de940ec5..2be5f4b83a23d 100644 --- a/x-pack/plugins/cases/public/components/actions/copy_id/use_copy_id_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/copy_id/use_copy_id_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useCopyIDAction } from './use_copy_id_action'; import { basicCase } from '../../../containers/mock'; @@ -58,7 +58,7 @@ describe('useCopyIDAction', () => { }); it('copies the id of the selected case to the clipboard', async () => { - const { result, waitFor } = renderHook(() => useCopyIDAction({ onActionSuccess }), { + const { result } = renderHook(() => useCopyIDAction({ onActionSuccess }), { wrapper: appMockRender.AppWrapper, }); @@ -73,7 +73,7 @@ describe('useCopyIDAction', () => { }); it('shows the success toaster correctly when copying the case id', async () => { - const { result, waitFor } = renderHook(() => useCopyIDAction({ onActionSuccess }), { + const { result } = renderHook(() => useCopyIDAction({ onActionSuccess }), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/components/actions/delete/use_delete_action.test.tsx b/x-pack/plugins/cases/public/components/actions/delete/use_delete_action.test.tsx index 9730783f39af6..fee612cbf04f7 100644 --- a/x-pack/plugins/cases/public/components/actions/delete/use_delete_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/delete/use_delete_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useDeleteAction } from './use_delete_action'; import * as api from '../../../containers/api'; @@ -84,7 +84,7 @@ describe('useDeleteAction', () => { it('deletes the selected cases', async () => { const deleteSpy = jest.spyOn(api, 'deleteCases'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useDeleteAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -112,7 +112,7 @@ describe('useDeleteAction', () => { }); it('closes the modal', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useDeleteAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -137,7 +137,7 @@ describe('useDeleteAction', () => { }); it('shows the success toaster correctly when delete one case', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useDeleteAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -163,7 +163,7 @@ describe('useDeleteAction', () => { }); it('shows the success toaster correctly when delete multiple case', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useDeleteAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, diff --git a/x-pack/plugins/cases/public/components/actions/severity/use_severity_action.test.tsx b/x-pack/plugins/cases/public/components/actions/severity/use_severity_action.test.tsx index 79ae67610d902..93982ff334c22 100644 --- a/x-pack/plugins/cases/public/components/actions/severity/use_severity_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/severity/use_severity_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useSeverityAction } from './use_severity_action'; import * as api from '../../../containers/api'; @@ -80,7 +80,7 @@ describe('useSeverityAction', () => { it('update the severity cases', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -120,7 +120,7 @@ describe('useSeverityAction', () => { it.each(singleCaseTests)( 'shows the success toaster correctly when updating the severity of the case: %s', async (_, index, expectedMessage) => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -153,7 +153,7 @@ describe('useSeverityAction', () => { it.each(multipleCasesTests)( 'shows the success toaster correctly when updating the severity of the case: %s', async (_, index, expectedMessage) => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, diff --git a/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx b/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx index 5ad7f9803dd67..9a007e5ea28a9 100644 --- a/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useStatusAction } from './use_status_action'; import * as api from '../../../containers/api'; @@ -82,7 +82,7 @@ describe('useStatusAction', () => { it('update the status cases', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -120,7 +120,7 @@ describe('useStatusAction', () => { it.each(singleCaseTests)( 'shows the success toaster correctly when updating the status of the case: %s', async (_, index, expectedMessage) => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -152,7 +152,7 @@ describe('useStatusAction', () => { it.each(multipleCasesTests)( 'shows the success toaster correctly when updating the status of the case: %s', async (_, index, expectedMessage) => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, diff --git a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx index 14973cc59be78..dbe2a1cc17aa5 100644 --- a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useTagsAction } from './use_tags_action'; import * as api from '../../../containers/api'; @@ -56,7 +56,7 @@ describe('useTagsAction', () => { it('update the tags correctly', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -86,7 +86,7 @@ describe('useTagsAction', () => { }); it('shows the success toaster correctly when updating one case', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, @@ -112,7 +112,7 @@ describe('useTagsAction', () => { }); it('shows the success toaster correctly when updating multiple cases', async () => { - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), { wrapper: appMockRender.AppWrapper, diff --git a/x-pack/plugins/cases/public/components/actions/use_items_action.test.tsx b/x-pack/plugins/cases/public/components/actions/use_items_action.test.tsx index 25a08007ac31a..b1f24562f89bd 100644 --- a/x-pack/plugins/cases/public/components/actions/use_items_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/use_items_action.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useItemsAction } from './use_items_action'; import * as api from '../../containers/api'; @@ -54,7 +54,7 @@ describe('useItemsAction', () => { }); it('closes the flyout', async () => { - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -81,7 +81,7 @@ describe('useItemsAction', () => { it('update the items correctly', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -117,7 +117,7 @@ describe('useItemsAction', () => { }); it('calls fieldSelector correctly', async () => { - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -142,7 +142,7 @@ describe('useItemsAction', () => { }); it('calls itemsTransformer correctly', async () => { - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -169,7 +169,7 @@ describe('useItemsAction', () => { it('removes duplicates', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -203,7 +203,7 @@ describe('useItemsAction', () => { }); it('shows the success toaster correctly when updating a case', async () => { - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -229,7 +229,7 @@ describe('useItemsAction', () => { it('do not update cases with no changes', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -254,7 +254,7 @@ describe('useItemsAction', () => { it('do not update if the selected items are the same but with different order', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -279,7 +279,7 @@ describe('useItemsAction', () => { it('do not update if the selected items are the same', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -304,7 +304,7 @@ describe('useItemsAction', () => { it('do not update if selecting and unselecting the same item', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); @@ -329,7 +329,7 @@ describe('useItemsAction', () => { it('do not update with empty items and no selection', async () => { const updateSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor } = renderHook(() => useItemsAction(props), { + const { result } = renderHook(() => useItemsAction(props), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/components/actions/use_items_state.test.tsx b/x-pack/plugins/cases/public/components/actions/use_items_state.test.tsx index a680ec655652a..e0f07eaf1a5cd 100644 --- a/x-pack/plugins/cases/public/components/actions/use_items_state.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/use_items_state.test.tsx @@ -7,7 +7,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useItemsState } from './use_items_state'; import { basicCase } from '../../containers/mock'; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index bc540040cce57..5c79aadbcfeeb 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import moment from 'moment-timezone'; -import { render, waitFor, screen, within } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; +import { render, waitFor, screen, within, renderHook } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -27,7 +26,6 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { getEmptyCellValue } from '../empty_value'; import { useKibana } from '../../common/lib/kibana'; import { AllCasesList } from './all_cases_list'; -import type { GetCasesColumn, UseCasesColumnsReturnValue } from './use_cases_columns'; import { useCasesColumns } from './use_cases_columns'; import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; @@ -267,10 +265,7 @@ describe.skip('AllCasesListGeneric', () => { expect(column[key].querySelector('span')).toHaveTextContent(emptyTag); }; - const { result } = renderHook< - React.PropsWithChildren, - UseCasesColumnsReturnValue - >(() => useCasesColumns(defaultColumnArgs), { + const { result } = renderHook(() => useCasesColumns(defaultColumnArgs), { wrapper: ({ children }) => {children}, }); diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx index 9105f82476891..9fd5f53af2f20 100644 --- a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx @@ -10,27 +10,22 @@ import React from 'react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { CasesMetrics } from './cases_metrics'; jest.mock('pretty-ms', () => jest.fn().mockReturnValue('2ms')); jest.mock('../../containers/use_get_cases_metrics'); -jest.mock('../../containers/use_get_cases_status'); const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; describe('Cases metrics', () => { let appMockRenderer: AppMockRenderer; beforeEach(() => { - useGetCasesMetricsMock.mockReturnValue({ isLoading: false, data: { mttr: 2000 } }); - useGetCasesStatusMock.mockReturnValue({ + useGetCasesMetricsMock.mockReturnValue({ isLoading: false, data: { - countOpenCases: 20, - countInProgressCases: 40, - countClosedCases: 130, + mttr: 2000, + status: { open: 20, inProgress: 40, closed: 130 }, }, }); diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx index 3d883550bd4a8..525b061965ffd 100644 --- a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx @@ -17,22 +17,13 @@ import { } from '@elastic/eui'; import prettyMilliseconds from 'pretty-ms'; import { CaseStatuses } from '../../../common/types/domain'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { StatusStats } from '../status/status_stats'; import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; import { ATTC_DESCRIPTION, ATTC_STAT, ATTC_STAT_INFO_ARIA_LABEL } from './translations'; export const CasesMetrics: React.FC = () => { - const { - data: { countOpenCases, countInProgressCases, countClosedCases } = { - countOpenCases: 0, - countInProgressCases: 0, - countClosedCases: 0, - }, - isLoading: isCasesStatusLoading, - } = useGetCasesStatus(); - - const { data: { mttr } = { mttr: 0 }, isLoading: isCasesMetricsLoading } = useGetCasesMetrics(); + const { data: { mttr, status } = { mttr: 0 }, isLoading: isCasesMetricsLoading } = + useGetCasesMetrics(); const mttrValue = useMemo( () => (mttr != null ? prettyMilliseconds(mttr * 1000, { compact: true, verbose: false }) : '-'), @@ -46,25 +37,25 @@ export const CasesMetrics: React.FC = () => { diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index 644c67b632df1..dbe7412a5d7b5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { waitFor } from '@testing-library/react'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { FC, PropsWithChildren } from 'react'; import React from 'react'; @@ -96,12 +95,11 @@ describe('use cases add to existing case modal hook', () => { }); it('should throw if called outside of a cases context', () => { - const { result } = renderHook(() => { - useCasesAddToExistingCaseModal(defaultParams()); - }); - expect(result.error?.message).toContain( - 'useCasesContext must be used within a CasesProvider and have a defined value' - ); + expect(() => + renderHook(() => { + useCasesAddToExistingCaseModal(defaultParams()); + }) + ).toThrow(/useCasesContext must be used within a CasesProvider and have a defined value/); }); it('should dispatch the open action when invoked', () => { diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx index 89419d587237c..c38486dfb7ea4 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; import type { FilterConfig, FilterConfigRenderParams } from './types'; @@ -64,7 +64,7 @@ describe('useFilterConfig', () => { it('should remove a selected option if the filter is deleted', async () => { const { rerender } = renderHook(useFilterConfig, { - wrapper: ({ children }: React.PropsWithChildren[0]>) => ( + wrapper: ({ children }: React.PropsWithChildren) => ( {children} ), initialProps: { @@ -106,7 +106,7 @@ describe('useFilterConfig', () => { ); const { result } = renderHook(useFilterConfig, { - wrapper: ({ children }: React.PropsWithChildren[0]>) => ( + wrapper: ({ children }: React.PropsWithChildren) => ( {children} ), initialProps: { diff --git a/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx index bb0ba6ed009e1..e98926bcd0b40 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx @@ -6,8 +6,7 @@ */ import userEvent, { type UserEvent } from '@testing-library/user-event'; -import { waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor, renderHook } from '@testing-library/react'; import { waitForEuiPopoverOpen, waitForEuiContextMenuPanelTransition, diff --git a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx index 1e257c8fbcefd..511edce760a48 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook, act } from '@testing-library/react'; import { CaseStatuses } from '@kbn/cases-components'; import { TestProviders } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx index 1838ee3b14f59..2bbd7da38545d 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiContextMenu } from '@elastic/eui'; import userEvent from '@testing-library/user-event'; -import { waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor, renderHook } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { @@ -190,7 +189,7 @@ describe('useBulkActions', () => { it('change the status of cases', async () => { const updateCasesSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -219,7 +218,7 @@ describe('useBulkActions', () => { pointerEventsCheck: 0, }); - await waitForHook(() => { + await waitFor(() => { expect(updateCasesSpy).toHaveBeenCalled(); }); }); @@ -227,7 +226,7 @@ describe('useBulkActions', () => { it('change the severity of cases', async () => { const updateCasesSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -257,7 +256,7 @@ describe('useBulkActions', () => { pointerEventsCheck: 0, }); - await waitForHook(() => { + await waitFor(() => { expect(updateCasesSpy).toHaveBeenCalled(); }); }); @@ -266,7 +265,7 @@ describe('useBulkActions', () => { it('delete a case', async () => { const deleteSpy = jest.spyOn(api, 'deleteCases'); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -299,7 +298,7 @@ describe('useBulkActions', () => { await userEvent.click(res.getByTestId('confirmModalConfirmButton')); - await waitForHook(() => { + await waitFor(() => { expect(deleteSpy).toHaveBeenCalled(); }); }); @@ -355,7 +354,7 @@ describe('useBulkActions', () => { it('change the tags of the case', async () => { const updateCasesSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -394,7 +393,7 @@ describe('useBulkActions', () => { await userEvent.click(res.getByText('coke')); await userEvent.click(res.getByTestId('cases-edit-tags-flyout-submit')); - await waitForHook(() => { + await waitFor(() => { expect(updateCasesSpy).toHaveBeenCalled(); }); }); @@ -402,7 +401,7 @@ describe('useBulkActions', () => { it('change the assignees of the case', async () => { const updateCasesSpy = jest.spyOn(api, 'updateCases'); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -441,7 +440,7 @@ describe('useBulkActions', () => { await userEvent.click(res.getByText('Damaged Raccoon')); await userEvent.click(res.getByTestId('cases-edit-assignees-flyout-submit')); - await waitForHook(() => { + await waitFor(() => { expect(updateCasesSpy).toHaveBeenCalled(); }); }); @@ -450,7 +449,7 @@ describe('useBulkActions', () => { describe('Permissions', () => { it('shows the correct actions with all permissions', async () => { appMockRender = createAppMockRenderer({ permissions: allCasesPermissions() }); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -467,7 +466,7 @@ describe('useBulkActions', () => { ); - await waitForHook(() => { + await waitFor(() => { expect(res.getByTestId('case-bulk-action-status')).toBeInTheDocument(); expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument(); expect(res.getByTestId('bulk-actions-separator')).toBeInTheDocument(); @@ -476,7 +475,7 @@ describe('useBulkActions', () => { it('shows the correct actions with no delete permissions', async () => { appMockRender = createAppMockRenderer({ permissions: noDeleteCasesPermissions() }); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -493,7 +492,7 @@ describe('useBulkActions', () => { ); - await waitForHook(() => { + await waitFor(() => { expect(res.getByTestId('case-bulk-action-status')).toBeInTheDocument(); expect(res.queryByTestId('cases-bulk-action-delete')).toBeFalsy(); expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy(); @@ -502,7 +501,7 @@ describe('useBulkActions', () => { it('shows the correct actions with only delete permissions', async () => { appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() }); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }), { wrapper: appMockRender.AppWrapper, @@ -519,7 +518,7 @@ describe('useBulkActions', () => { ); - await waitForHook(() => { + await waitFor(() => { expect(res.queryByTestId('case-bulk-action-status')).toBeFalsy(); expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument(); expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy(); @@ -528,7 +527,7 @@ describe('useBulkActions', () => { it('shows the correct actions with no reopen permissions', async () => { appMockRender = createAppMockRenderer({ permissions: noReopenCasesPermissions() }); - const { result, waitFor: waitForHook } = renderHook( + const { result } = renderHook( () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCaseClosed] }), { wrapper: appMockRender.AppWrapper, @@ -545,12 +544,12 @@ describe('useBulkActions', () => { ); - await waitForHook(() => { + await waitFor(() => { expect(res.queryByTestId('case-bulk-action-status')).toBeInTheDocument(); res.queryByTestId('case-bulk-action-status')?.click(); }); - await waitForHook(() => { + await waitFor(() => { expect(res.queryByTestId('cases-bulk-action-status-open')).toBeDisabled(); expect(res.queryByTestId('cases-bulk-action-status-in-progress')).toBeDisabled(); expect(res.queryByTestId('cases-bulk-action-status-closed')).toBeDisabled(); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx index 22783cf05cfc1..550240060ddf6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx @@ -15,7 +15,7 @@ import { useGetCasesMockState } from '../../containers/mock'; import { connectors, useCaseConfigureResponse } from '../configure_cases/__mock__'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, readCasesPermissions, TestProviders } from '../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { CaseStatuses, CustomFieldTypes } from '../../../common/types/domain'; import { userProfilesMap } from '../../containers/user_profiles/api.mock'; import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration'; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_configuration.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_configuration.test.tsx index 761da1d6316e8..4a4a139445c5e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_configuration.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_configuration.test.tsx @@ -6,7 +6,7 @@ */ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.test.tsx index 26f0f8c2fb85e..e6603a35a1a04 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.test.tsx @@ -6,7 +6,7 @@ */ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx index 484fabca00c3d..338055e5da0cd 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { casesQueriesKeys } from '../../containers/constants'; diff --git a/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts b/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts index 4cd015de0c92e..5a19e9a0f995b 100644 --- a/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts +++ b/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { APP_ID, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; diff --git a/x-pack/plugins/cases/public/components/app/use_readonly_header.test.tsx b/x-pack/plugins/cases/public/components/app/use_readonly_header.test.tsx index 9be5a6336b3c2..69d7a9a8b65a8 100644 --- a/x-pack/plugins/cases/public/components/app/use_readonly_header.test.tsx +++ b/x-pack/plugins/cases/public/components/app/use_readonly_header.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useKibana } from '../../common/lib/kibana'; import { readCasesPermissions, TestProviders } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/components/case_form_fields/severity.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/severity.test.tsx index 12f72a073b7fc..2034d3c4099ac 100644 --- a/x-pack/plugins/cases/public/components/case_form_fields/severity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_form_fields/severity.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { screen, waitFor } from '@testing-library/react'; -import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; +import { render, screen, waitFor } from '@testing-library/react'; import { Severity } from './severity'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -16,20 +14,9 @@ import { FormTestComponent } from '../../common/test_utils'; const onSubmit = jest.fn(); -// FLAKY: https://github.com/elastic/kibana/issues/188951 -describe.skip('Severity form field', () => { - let appMockRender: AppMockRenderer; - - beforeEach(() => { - appMockRender = createAppMockRenderer(); - }); - - afterEach(async () => { - await appMockRender.clearQueryCache(); - }); - +describe('Severity form field', () => { it('renders', async () => { - appMockRender.render( + render( @@ -41,7 +28,7 @@ describe.skip('Severity form field', () => { // default to LOW in this test configuration it('defaults to the correct value', async () => { - appMockRender.render( + render( @@ -52,7 +39,7 @@ describe.skip('Severity form field', () => { }); it('selects the correct value when changed', async () => { - appMockRender.render( + render( @@ -74,7 +61,7 @@ describe.skip('Severity form field', () => { }); it('disables when loading data', async () => { - appMockRender.render( + render( diff --git a/x-pack/plugins/cases/public/components/case_form_fields/sync_alerts_toggle.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/sync_alerts_toggle.test.tsx index fbe7eca218391..238abfefeb474 100644 --- a/x-pack/plugins/cases/public/components/case_form_fields/sync_alerts_toggle.test.tsx +++ b/x-pack/plugins/cases/public/components/case_form_fields/sync_alerts_toggle.test.tsx @@ -33,9 +33,10 @@ describe('SyncAlertsToggle', () => { ); - expect(await screen.findByTestId('caseSyncAlerts')).toBeInTheDocument(); - expect(await screen.findByRole('switch')).toHaveAttribute('aria-checked', 'true'); - expect(await screen.findByText('On')).toBeInTheDocument(); + const syncAlerts = await screen.findByTestId('caseSyncAlerts'); + expect(syncAlerts).toBeInTheDocument(); + expect(within(syncAlerts).getByRole('switch')).toHaveAttribute('aria-checked', 'true'); + expect(within(syncAlerts).getByText('On')).toBeInTheDocument(); }); it('it toggles the switch', async () => { @@ -45,9 +46,9 @@ describe('SyncAlertsToggle', () => { ); - const synAlerts = await screen.findByTestId('caseSyncAlerts'); + const syncAlerts = await screen.findByTestId('caseSyncAlerts'); - await userEvent.click(within(synAlerts).getByRole('switch')); + await userEvent.click(within(syncAlerts).getByRole('switch')); expect(await screen.findByRole('switch')).toHaveAttribute('aria-checked', 'false'); expect(await screen.findByText('Off')).toBeInTheDocument(); @@ -60,9 +61,9 @@ describe('SyncAlertsToggle', () => { ); - const synAlerts = await screen.findByTestId('caseSyncAlerts'); + const syncAlerts = await screen.findByTestId('caseSyncAlerts'); - await userEvent.click(within(synAlerts).getByRole('switch')); + await userEvent.click(within(syncAlerts).getByRole('switch')); await userEvent.click(await screen.findByText('Submit')); diff --git a/x-pack/plugins/cases/public/components/case_form_fields/title.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/title.test.tsx index e277a992c6bda..e861b4a3babe9 100644 --- a/x-pack/plugins/cases/public/components/case_form_fields/title.test.tsx +++ b/x-pack/plugins/cases/public/components/case_form_fields/title.test.tsx @@ -7,7 +7,7 @@ import type { FC, PropsWithChildren } from 'react'; import React from 'react'; -import { screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { CaseFormFieldsSchemaProps } from './schema'; @@ -17,11 +17,9 @@ import userEvent from '@testing-library/user-event'; import { Title } from './title'; import { schema } from '../create/schema'; -import { createAppMockRenderer, type AppMockRenderer } from '../../common/mock'; describe('Title', () => { let globalForm: FormHook; - let appMockRender: AppMockRenderer; const MockHookWrapperComponent: FC> = ({ children }) => { const { form } = useForm({ @@ -38,11 +36,10 @@ describe('Title', () => { beforeEach(() => { jest.resetAllMocks(); - appMockRender = createAppMockRenderer(); }); it('it renders', async () => { - appMockRender.render( + render( </MockHookWrapperComponent> @@ -52,7 +49,7 @@ describe('Title', () => { }); it('it disables the input when loading', async () => { - appMockRender.render( + render( <MockHookWrapperComponent> <Title isLoading={true} /> </MockHookWrapperComponent> @@ -61,7 +58,7 @@ describe('Title', () => { }); it('it changes the title', async () => { - appMockRender.render( + render( <MockHookWrapperComponent> <Title isLoading={false} /> </MockHookWrapperComponent> diff --git a/x-pack/plugins/cases/public/components/cases_context/state/use_is_add_to_case_open.test.tsx b/x-pack/plugins/cases/public/components/cases_context/state/use_is_add_to_case_open.test.tsx index 9974d0cb530d9..61cf355229664 100644 --- a/x-pack/plugins/cases/public/components/cases_context/state/use_is_add_to_case_open.test.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/state/use_is_add_to_case_open.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import { createAppMockRenderer } from '../../../common/mock'; import { useIsAddToCaseOpen } from './use_is_add_to_case_open'; @@ -26,9 +26,8 @@ describe('use is add to existing case modal open hook', () => { }); it('should throw if called outside of a cases context', () => { - const { result } = renderHook(useIsAddToCaseOpen); - expect(result.error?.message).toContain( - 'useCasesStateContext must be used within a CasesProvider and have a defined value' + expect(() => renderHook(useIsAddToCaseOpen)).toThrow( + /useCasesStateContext must be used within a CasesProvider and have a defined value/ ); }); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx index f1cb277f1a24b..b529139644d53 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetFieldsByIssueType', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getFieldsByIssueType'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetFieldsByIssueType({ http, @@ -88,7 +88,7 @@ describe('useGetFieldsByIssueType', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetFieldsByIssueType({ http, @@ -114,7 +114,7 @@ describe('useGetFieldsByIssueType', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetFieldsByIssueType({ http, diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx index 876738025e6a8..04f1995f6cc8f 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector as actionConnector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetIssue', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getIssue'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssue({ http, @@ -40,7 +40,7 @@ describe('useGetIssue', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isSuccess); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(spy).toHaveBeenCalledWith({ http, @@ -88,7 +88,7 @@ describe('useGetIssue', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssue({ http, @@ -98,9 +98,10 @@ describe('useGetIssue', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isError); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.isError).toBe(true); + expect(addError).toHaveBeenCalled(); + }); }); it('calls addError when the getIssue api returns successfully but contains an error', async () => { @@ -114,7 +115,7 @@ describe('useGetIssue', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssue({ http, @@ -124,8 +125,9 @@ describe('useGetIssue', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isSuccess); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx index 0d7e3127dd9fe..dde59c2dd64bb 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetIssueTypes', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getIssueTypes'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssueTypes({ http, @@ -70,7 +70,7 @@ describe('useGetIssueTypes', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetIssueTypes({ http, @@ -95,7 +95,7 @@ describe('useGetIssueTypes', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetIssueTypes({ http, diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx index a06cd4391f766..b43a231e4eb0b 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector as actionConnector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetIssues', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getIssues'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssues({ http, @@ -40,13 +40,14 @@ describe('useGetIssues', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isSuccess); - - expect(spy).toHaveBeenCalledWith({ - http, - signal: expect.anything(), - connectorId: actionConnector.id, - title: 'Task', + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(spy).toHaveBeenCalledWith({ + http, + signal: expect.anything(), + connectorId: actionConnector.id, + title: 'Task', + }); }); }); @@ -74,7 +75,7 @@ describe('useGetIssues', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssues({ http, @@ -84,9 +85,10 @@ describe('useGetIssues', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isError); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.isError).toBe(true); + expect(addError).toHaveBeenCalled(); + }); }); it('calls addError when the getIssues api returns successfully but contains an error', async () => { @@ -100,7 +102,7 @@ describe('useGetIssues', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIssues({ http, @@ -110,8 +112,9 @@ describe('useGetIssues', () => { { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isSuccess); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx index 7bd0c16a6a4d5..bfe20b4dc4dea 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetIncidentTypes', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getIncidentTypes'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetIncidentTypes({ http, @@ -70,7 +70,7 @@ describe('useGetIncidentTypes', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetIncidentTypes({ http, @@ -95,7 +95,7 @@ describe('useGetIncidentTypes', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetIncidentTypes({ http, diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx index 6f59b4d50c31c..71d09a0cc68e9 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import { connector } from '../mock'; @@ -30,7 +30,7 @@ describe('useGetSeverity', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getSeverity'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useGetSeverity({ http, @@ -70,7 +70,7 @@ describe('useGetSeverity', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetSeverity({ http, @@ -95,7 +95,7 @@ describe('useGetSeverity', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetSeverity({ http, diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx index 1508817619501..3f44f4c30f7cf 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana, useToasts } from '../../../common/lib/kibana'; import type { ActionConnector } from '../../../../common/types/domain'; @@ -47,7 +47,7 @@ describe('useGetChoices', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getChoices'); - const { waitFor } = renderHook( + renderHook( () => useGetChoices({ http, @@ -92,7 +92,7 @@ describe('useGetChoices', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetChoices({ http, @@ -118,7 +118,7 @@ describe('useGetChoices', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError }); - const { waitFor } = renderHook( + renderHook( () => useGetChoices({ http, diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx index 168cae0e478fc..0d1ad5b8b65b6 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx @@ -6,7 +6,7 @@ */ import { alertComment } from '../../../containers/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import { CasesContext } from '../../cases_context'; @@ -47,12 +47,11 @@ describe('use cases add to new case flyout hook', () => { }); it('should throw if called outside of a cases context', () => { - const { result } = renderHook(() => { - useCasesAddToNewCaseFlyout(); - }); - expect(result.error?.message).toContain( - 'useCasesContext must be used within a CasesProvider and have a defined value' - ); + expect(() => + renderHook(() => { + useCasesAddToNewCaseFlyout(); + }) + ).toThrow(/useCasesContext must be used within a CasesProvider and have a defined value/); }); it('should dispatch the open action when invoked without attachments', () => { diff --git a/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx index 4174d33c44d2f..080009feb1847 100644 --- a/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx +++ b/x-pack/plugins/cases/public/components/create/use_cancel_creation_action.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { useCancelCreationAction } from './use_cancel_creation_action'; diff --git a/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx index 49e18fb818cd9..f5a502f490775 100644 --- a/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/use_file_preview.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useFilePreview } from './use_file_preview'; diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx index 0467bb7a2efee..a064667f93c0f 100644 --- a/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.test.tsx @@ -9,7 +9,7 @@ import type { FilesTableColumnsProps } from './use_files_table_columns'; import { useFilesTableColumns } from './use_files_table_columns'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { basicCase } from '../../containers/mock'; describe('useFilesTableColumns', () => { diff --git a/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx index 77e5593671c7a..c45599860a560 100644 --- a/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx +++ b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx @@ -6,31 +6,21 @@ */ import React from 'react'; -import { waitForEuiPopoverOpen, screen } from '@elastic/eui/lib/test/rtl'; -import { waitFor } from '@testing-library/react'; +import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; +import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; - -import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; - import { FilterPopover } from '.'; describe('FilterPopover ', () => { - let appMockRender: AppMockRenderer; const onSelectedOptionsChanged = jest.fn(); const tags: string[] = ['coke', 'pepsi']; beforeEach(() => { - appMockRender = createAppMockRenderer(); jest.clearAllMocks(); }); - afterEach(async () => { - await appMockRender.clearQueryCache(); - }); - it('renders button label correctly', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -43,7 +33,7 @@ describe('FilterPopover ', () => { }); it('renders empty label correctly', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -61,7 +51,7 @@ describe('FilterPopover ', () => { }); it('renders string type options correctly', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -79,7 +69,7 @@ describe('FilterPopover ', () => { }); it('should call onSelectionChange with selected option', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -100,7 +90,7 @@ describe('FilterPopover ', () => { }); it('should call onSelectionChange with empty array when option is deselected', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -126,7 +116,7 @@ describe('FilterPopover ', () => { const maxLengthLabel = `You have selected maximum number of ${maxLength} tags to filter`; it('should show message when maximum options are selected', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -152,7 +142,7 @@ describe('FilterPopover ', () => { }); it('should not show message when maximum length label is missing', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -176,7 +166,7 @@ describe('FilterPopover ', () => { }); it('should not show message and disable options when maximum length property is missing', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} @@ -198,7 +188,7 @@ describe('FilterPopover ', () => { }); it('should allow to select more options when maximum length property is missing', async () => { - appMockRender.render( + render( <FilterPopover buttonLabel={'Tags'} onSelectedOptionsChanged={onSelectedOptionsChanged} diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx index e4ce68ed45237..06a92712f63d2 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor, renderHook, act } from '@testing-library/react'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import type { SessionStorageType } from './use_markdown_session_storage'; import { useMarkdownSessionStorage } from './use_markdown_session_storage'; -import { waitForComponentToUpdate } from '../../common/test_utils'; describe('useMarkdownSessionStorage', () => { const field = { @@ -45,7 +43,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should return hasConflicts as false', async () => { - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useMarkdownSessionStorage({ field, sessionKey, initialValue }) ); @@ -55,7 +53,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should return hasConflicts as false when sessionKey is empty', async () => { - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useMarkdownSessionStorage({ field, sessionKey: '', initialValue }) ); @@ -66,7 +64,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should update the session value with field value when it is first render', async () => { - const { waitFor } = renderHook<SessionStorageType, { hasConflicts: boolean }>( + renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -86,7 +84,7 @@ describe('useMarkdownSessionStorage', () => { it('should set session storage when field has value and session key is not created yet', async () => { const specialCharsValue = '!{tooltip[Hello again](This is tooltip!)}'; - const { waitFor, result } = renderHook<SessionStorageType, { hasConflicts: boolean }>( + const { result } = renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -101,8 +99,6 @@ describe('useMarkdownSessionStorage', () => { jest.advanceTimersByTime(1000); }); - await waitForComponentToUpdate(); - await waitFor(() => { expect(result.current.hasConflicts).toBe(false); expect(sessionStorage.getItem(sessionKey)).toBe(specialCharsValue); @@ -110,7 +106,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should update session value ', async () => { - const { result, rerender, waitFor } = renderHook<SessionStorageType, { hasConflicts: boolean }>( + const { result, rerender } = renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -129,8 +125,6 @@ describe('useMarkdownSessionStorage', () => { jest.advanceTimersByTime(1000); }); - await waitForComponentToUpdate(); - await waitFor(() => { expect(result.current.hasConflicts).toBe(false); expect(sessionStorage.getItem(sessionKey)).toBe('new value'); @@ -138,7 +132,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should return has conflict true', async () => { - const { result, rerender, waitFor } = renderHook<SessionStorageType, { hasConflicts: boolean }>( + const { result, rerender } = renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -162,7 +156,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should set field value if session already exists and it is a first render', async () => { - const { waitFor, result } = renderHook<SessionStorageType, { hasConflicts: boolean }>( + const { result } = renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -171,8 +165,6 @@ describe('useMarkdownSessionStorage', () => { } ); - await waitForComponentToUpdate(); - await waitFor(() => { expect(field.setValue).toHaveBeenCalled(); }); @@ -181,8 +173,6 @@ describe('useMarkdownSessionStorage', () => { jest.advanceTimersByTime(1000); }); - await waitForComponentToUpdate(); - await waitFor(() => { expect(result.current.hasConflicts).toBe(false); expect(field.value).toBe(sessionStorage.getItem(sessionKey)); @@ -190,10 +180,7 @@ describe('useMarkdownSessionStorage', () => { }); it('should update existing session key if field value changed', async () => { - const { waitFor, rerender, result } = renderHook< - SessionStorageType, - { hasConflicts: boolean } - >( + const { rerender, result } = renderHook( (props) => { return useMarkdownSessionStorage(props); }, @@ -202,8 +189,6 @@ describe('useMarkdownSessionStorage', () => { } ); - await waitForComponentToUpdate(); - await waitFor(() => { expect(field.setValue).toHaveBeenCalled(); }); @@ -218,8 +203,6 @@ describe('useMarkdownSessionStorage', () => { jest.advanceTimersByTime(1000); }); - await waitForComponentToUpdate(); - await waitFor(() => { expect(result.current.hasConflicts).toBe(false); expect(sessionStorage.getItem(sessionKey)).toBe('new value'); diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx index 9f48783fde24d..b494bd1d81839 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../common/mock'; import { useCasesBreadcrumbs, useCasesTitleBreadcrumbs } from '.'; import { CasesDeepLinkId } from '../../common/navigation'; diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx index dcef6d26393aa..56599299fd0a7 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx @@ -6,10 +6,10 @@ */ import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; import { useKibana } from '../../common/lib/kibana'; -import type { UseCreateCaseModalProps, UseCreateCaseModalReturnedValues } from '.'; import { useCreateCaseModal } from '.'; import { TestProviders } from '../../common/mock'; @@ -27,10 +27,7 @@ describe('useCreateCaseModal', () => { }); it('init', async () => { - const { result } = renderHook< - React.PropsWithChildren<UseCreateCaseModalProps>, - UseCreateCaseModalReturnedValues - >(() => useCreateCaseModal({ onCaseCreated }), { + const { result } = renderHook(() => useCreateCaseModal({ onCaseCreated }), { wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, }); @@ -38,10 +35,7 @@ describe('useCreateCaseModal', () => { }); it('opens the modal', async () => { - const { result } = renderHook< - React.PropsWithChildren<UseCreateCaseModalProps>, - UseCreateCaseModalReturnedValues - >(() => useCreateCaseModal({ onCaseCreated }), { + const { result } = renderHook(() => useCreateCaseModal({ onCaseCreated }), { wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, }); @@ -53,10 +47,7 @@ describe('useCreateCaseModal', () => { }); it('closes the modal', async () => { - const { result } = renderHook< - React.PropsWithChildren<UseCreateCaseModalProps>, - UseCreateCaseModalReturnedValues - >(() => useCreateCaseModal({ onCaseCreated }), { + const { result } = renderHook(() => useCreateCaseModal({ onCaseCreated }), { wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, }); @@ -69,10 +60,7 @@ describe('useCreateCaseModal', () => { }); it('returns a memoized value', async () => { - const { result, rerender } = renderHook< - React.PropsWithChildren<UseCreateCaseModalProps>, - UseCreateCaseModalReturnedValues - >(() => useCreateCaseModal({ onCaseCreated }), { + const { result, rerender } = renderHook(() => useCreateCaseModal({ onCaseCreated }), { wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, }); @@ -84,10 +72,7 @@ describe('useCreateCaseModal', () => { }); it('closes the modal when creating a case', async () => { - const { result } = renderHook< - React.PropsWithChildren<UseCreateCaseModalProps>, - UseCreateCaseModalReturnedValues - >(() => useCreateCaseModal({ onCaseCreated }), { + const { result } = renderHook(() => useCreateCaseModal({ onCaseCreated }), { wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, }); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx index 75c2694f89479..02e1a99fd0631 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; -import type { ReturnUsePushToService, UsePushToService } from '.'; import { usePushToService } from '.'; import { noPushCasesPermissions, readCasesPermissions, TestProviders } from '../../common/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; @@ -67,10 +66,7 @@ describe('usePushToService', () => { }); it('calls pushCaseToExternalService with correct arguments', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -93,10 +89,7 @@ describe('usePushToService', () => { }, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -115,10 +108,7 @@ describe('usePushToService', () => { }, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -129,10 +119,7 @@ describe('usePushToService', () => { }); it('Displays message when user has select none as connector', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -155,10 +142,7 @@ describe('usePushToService', () => { }); it('Displays message when connector is deleted', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -182,10 +166,7 @@ describe('usePushToService', () => { }); it('should not call pushCaseToExternalService when the selected connector is none', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -209,10 +190,7 @@ describe('usePushToService', () => { }); it('refresh case view page after push', async () => { - const { result, waitFor } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -227,10 +205,7 @@ describe('usePushToService', () => { describe('user does not have write or push permissions', () => { it('returns correct information about push permissions', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => ( <TestProviders permissions={noPushCasesPermissions()}> {children}</TestProviders> ), @@ -248,10 +223,7 @@ describe('usePushToService', () => { }, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => ( <TestProviders permissions={readCasesPermissions()}> {children}</TestProviders> ), @@ -270,10 +242,7 @@ describe('usePushToService', () => { }, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => ( <TestProviders permissions={readCasesPermissions()}> {children}</TestProviders> ), @@ -284,10 +253,7 @@ describe('usePushToService', () => { }); it('does not display a message when user does not have any connector configured', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -310,10 +276,7 @@ describe('usePushToService', () => { }); it('does not display a message when user does have a connector but is configured to none', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -336,10 +299,7 @@ describe('usePushToService', () => { }); it('does not display a message when connector is deleted', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -363,10 +323,7 @@ describe('usePushToService', () => { }); it('does not display a message when case is closed', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -386,10 +343,7 @@ describe('usePushToService', () => { describe('returned values', () => { it('initial', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -408,10 +362,7 @@ describe('usePushToService', () => { it('isLoading is true when usePostPushToService is loading', async () => { usePostPushToServiceMock.mockReturnValue({ ...mockPostPush, isLoading: true }); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -424,10 +375,7 @@ describe('usePushToService', () => { data: actionLicense, }); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -435,21 +383,18 @@ describe('usePushToService', () => { }); it('hasErrorMessages=true if there are error messages', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService({ ...defaultArgs, isValidConnector: false }), { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - }); + const { result } = renderHook( + () => usePushToService({ ...defaultArgs, isValidConnector: false }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); expect(result.current.hasErrorMessages).toBe(true); }); it('needsToBePushed=true if the connector needs to be pushed', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -473,10 +418,7 @@ describe('usePushToService', () => { }); it('needsToBePushed=false if the connector does not exist', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -497,10 +439,7 @@ describe('usePushToService', () => { }); it('hasBeenPushed=false if the connector has been pushed', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -524,10 +463,7 @@ describe('usePushToService', () => { }); it('hasBeenPushed=false if the connector does not exist', async () => { - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >( + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -553,10 +489,7 @@ describe('usePushToService', () => { data: actionLicense, }); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => ( <TestProviders permissions={noPushCasesPermissions()}> {children}</TestProviders> ), @@ -574,10 +507,7 @@ describe('usePushToService', () => { }, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); @@ -590,10 +520,7 @@ describe('usePushToService', () => { data: undefined, })); - const { result } = renderHook< - React.PropsWithChildren<UsePushToService>, - ReturnUsePushToService - >(() => usePushToService(defaultArgs), { + const { result } = renderHook(() => usePushToService(defaultArgs), { wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, }); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.test.tsx index 0a31b0cb875ad..0f485845fcd36 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/registered_attachments.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { AttachmentType, @@ -16,8 +16,6 @@ import { AttachmentActionType } from '../../../client/attachment_framework/types import { AttachmentTypeRegistry } from '../../../../common/registry'; import { getMockBuilderArgs } from '../mock'; import { createRegisteredAttachmentUserActionBuilder } from './registered_attachments'; -import type { AppMockRenderer } from '../../../common/mock'; -import { createAppMockRenderer } from '../../../common/mock'; const getLazyComponent = () => React.lazy(() => { @@ -32,8 +30,6 @@ const getLazyComponent = () => }); describe('createRegisteredAttachmentUserActionBuilder', () => { - let appMockRender: AppMockRenderer; - const attachmentTypeId = 'test'; const builderArgs = getMockBuilderArgs(); const registry = new AttachmentTypeRegistry<AttachmentType<CommonAttachmentViewProps>>( @@ -77,7 +73,6 @@ describe('createRegisteredAttachmentUserActionBuilder', () => { }; beforeEach(() => { - appMockRender = createAppMockRenderer(); jest.clearAllMocks(); }); @@ -161,8 +156,7 @@ describe('createRegisteredAttachmentUserActionBuilder', () => { const userAction = createRegisteredAttachmentUserActionBuilder(userActionBuilderArgs).build()[0]; - // @ts-expect-error: children is a proper React element - appMockRender.render(userAction.children); + render(userAction.children); expect(await screen.findByText('My component')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/property_actions/use_delete_property_action.test.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions/use_delete_property_action.test.tsx index 2db865ee3b22b..dba35699e2d8d 100644 --- a/x-pack/plugins/cases/public/components/user_actions/property_actions/use_delete_property_action.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions/use_delete_property_action.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; import { useDeletePropertyAction } from './use_delete_property_action'; diff --git a/x-pack/plugins/cases/public/components/user_actions/show_more_button.test.tsx b/x-pack/plugins/cases/public/components/user_actions/show_more_button.test.tsx index 34a0ad2713373..515cc5085f28c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/show_more_button.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/show_more_button.test.tsx @@ -6,31 +6,25 @@ */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ShowMoreButton } from './show_more_button'; -import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; const showMoreClickMock = jest.fn(); -// FLAKY: https://github.com/elastic/kibana/issues/192672 -describe.skip('ShowMoreButton', () => { - let appMockRender: AppMockRenderer; - +describe('ShowMoreButton', () => { beforeEach(() => { jest.clearAllMocks(); - appMockRender = createAppMockRenderer(); }); it('renders correctly', () => { - appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />); + render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />); expect(screen.getByTestId('cases-show-more-user-actions')).toBeInTheDocument(); }); it('shows loading state and is disabled when isLoading is true', () => { - appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} isLoading={true} />); + render(<ShowMoreButton onShowMoreClick={showMoreClickMock} isLoading={true} />); const btn = screen.getByTestId('cases-show-more-user-actions'); @@ -40,7 +34,7 @@ describe.skip('ShowMoreButton', () => { }); it('calls onShowMoreClick on button click', async () => { - appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />); + render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />); await userEvent.click(screen.getByTestId('cases-show-more-user-actions')); expect(showMoreClickMock).toHaveBeenCalled(); diff --git a/x-pack/plugins/cases/public/components/user_actions/use_last_page.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_last_page.test.tsx index 525fe19771e41..a05097e7f7b2b 100644 --- a/x-pack/plugins/cases/public/components/user_actions/use_last_page.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/use_last_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLastPage } from './use_last_page'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx index 3600a247540f5..4acd8ce0ee10e 100644 --- a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx @@ -7,7 +7,7 @@ import type { FC, PropsWithChildren } from 'react'; import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { basicCase } from '../../containers/mock'; import { useUpdateComment } from '../../containers/use_update_comment'; diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_last_page.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_last_page.test.tsx index 3207e4ffb13fe..ec50c60bb559c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_last_page.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_last_page.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useLastPageUserActions } from './use_user_actions_last_page'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; @@ -32,7 +32,7 @@ describe('useLastPageUserActions', () => { }); it('renders correctly', async () => { - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useLastPageUserActions({ lastPage: 5, userActivityQueryParams, @@ -79,7 +79,7 @@ describe('useLastPageUserActions', () => { it('returns loading state correctly', async () => { useFindCaseUserActionsMock.mockReturnValue({ isLoading: true }); - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useLastPageUserActions({ lastPage: 2, userActivityQueryParams, @@ -108,7 +108,7 @@ describe('useLastPageUserActions', () => { it('returns empty array when data is undefined', async () => { useFindCaseUserActionsMock.mockReturnValue({ isLoading: false, data: undefined }); - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useLastPageUserActions({ lastPage: 2, userActivityQueryParams, diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_pagination.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_pagination.test.tsx index 0d005a8b404fd..21702583e0292 100644 --- a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_pagination.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_pagination.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useUserActionsPagination } from './use_user_actions_pagination'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; @@ -32,7 +32,7 @@ describe('useUserActionsPagination', () => { }); it('renders expandable option correctly when user actions are more than 10', async () => { - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useUserActionsPagination({ userActivityQueryParams, caseId: basicCase.id, @@ -62,7 +62,7 @@ describe('useUserActionsPagination', () => { }); it('renders less than 10 user actions correctly', async () => { - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useUserActionsPagination({ userActivityQueryParams, caseId: basicCase.id, @@ -92,7 +92,7 @@ describe('useUserActionsPagination', () => { it('returns loading state correctly', async () => { useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: true }); - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useUserActionsPagination({ userActivityQueryParams, caseId: basicCase.id, @@ -124,7 +124,7 @@ describe('useUserActionsPagination', () => { it('returns empty array when data is undefined', async () => { useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: false, data: undefined }); - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useUserActionsPagination({ userActivityQueryParams, caseId: basicCase.id, @@ -161,7 +161,7 @@ describe('useUserActionsPagination', () => { }, }); - const { result, waitFor } = renderHook(() => + const { result } = renderHook(() => useUserActionsPagination({ userActivityQueryParams, caseId: basicCase.id, diff --git a/x-pack/plugins/cases/public/components/visualizations/actions/is_compatible.ts b/x-pack/plugins/cases/public/components/visualizations/actions/is_compatible.ts index 64becf44e266e..90347cb8d4067 100644 --- a/x-pack/plugins/cases/public/components/visualizations/actions/is_compatible.ts +++ b/x-pack/plugins/cases/public/components/visualizations/actions/is_compatible.ts @@ -7,7 +7,7 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { isLensApi } from '@kbn/lens-plugin/public'; -import { hasBlockingError } from '@kbn/presentation-publishing'; +import { apiPublishesTimeRange, hasBlockingError } from '@kbn/presentation-publishing'; import { canUseCases } from '../../../client/helpers/can_use_cases'; import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; @@ -20,7 +20,11 @@ export function isCompatible( if (!embeddable.getFullAttributes()) { return false; } - const timeRange = embeddable.timeRange$?.value ?? embeddable.parentApi?.timeRange$?.value; + const timeRange = + embeddable.timeRange$?.value ?? + (embeddable.parentApi && apiPublishesTimeRange(embeddable.parentApi) + ? embeddable.parentApi?.timeRange$?.value + : undefined); if (!timeRange) { return false; } diff --git a/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts b/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts index dea0c1ace09a7..94c7e5a1c939a 100644 --- a/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts +++ b/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts @@ -7,11 +7,11 @@ import { createBrowserHistory } from 'history'; import { BehaviorSubject } from 'rxjs'; - +import { getLensApiMock } from '@kbn/lens-plugin/public/react_embeddable/mocks'; import type { PublicAppInfo } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import type { LensApi, LensSavedObjectAttributes } from '@kbn/lens-plugin/public'; -import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import type { TimeRange } from '@kbn/es-query'; import type { Services } from './types'; const coreStart = coreMock.createStart(); @@ -39,24 +39,16 @@ export const mockLensAttributes = { export const getMockLensApi = ( { from, to = 'now' }: { from: string; to: string } = { from: 'now-24h', to: 'now' } ): LensApi => - ({ - type: 'lens', - getSavedVis: () => {}, - canViewUnderlyingData$: new BehaviorSubject(true), - getViewUnderlyingDataArgs: () => {}, + getLensApiMock({ getFullAttributes: () => { return mockLensAttributes; }, - panelTitle: new BehaviorSubject('myPanel'), - hidePanelTitle: new BehaviorSubject(false), - timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined), + panelTitle: new BehaviorSubject<string | undefined>('myPanel'), timeRange$: new BehaviorSubject<TimeRange | undefined>({ from, to, }), - filters$: new BehaviorSubject<Filter[] | undefined>(undefined), - query$: new BehaviorSubject<Query | AggregateQuery | undefined>(undefined), - } as unknown as LensApi); + }); export const getMockCurrentAppId$ = () => new BehaviorSubject<string>('securitySolutionUI'); export const getMockApplications$ = () => diff --git a/x-pack/plugins/cases/public/components/visualizations/actions/open_modal.tsx b/x-pack/plugins/cases/public/components/visualizations/actions/open_modal.tsx index 0542a13b1ef2d..c781098b44b57 100644 --- a/x-pack/plugins/cases/public/components/visualizations/actions/open_modal.tsx +++ b/x-pack/plugins/cases/public/components/visualizations/actions/open_modal.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useMemo } from 'react'; import { unmountComponentAtNode } from 'react-dom'; import type { LensApi } from '@kbn/lens-plugin/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; +import { apiPublishesTimeRange, useStateFromPublishingSubject } from '@kbn/presentation-publishing'; import { ActionWrapper } from './action_wrapper'; import type { CasesActionContextProps, Services } from './types'; import type { CaseUI } from '../../../../common'; @@ -30,7 +30,9 @@ const AddExistingCaseModalWrapper: React.FC<Props> = ({ lensApi, onClose, onSucc }); const timeRange = useStateFromPublishingSubject(lensApi.timeRange$); - const parentTimeRange = useStateFromPublishingSubject(lensApi.parentApi?.timeRange$); + const parentTimeRange = useStateFromPublishingSubject( + apiPublishesTimeRange(lensApi.parentApi) ? lensApi.parentApi?.timeRange$ : undefined + ); const absoluteTimeRange = convertToAbsoluteTimeRange(timeRange); const absoluteParentTimeRange = convertToAbsoluteTimeRange(parentTimeRange); diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index a24c8b5b677c5..2e792fbd134cc 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -10,7 +10,6 @@ import type { CasesFindResponseUI, CaseUI, CasesUI, - CasesStatus, FetchCasesProps, FindCaseUserActions, CaseUICustomField, @@ -24,7 +23,6 @@ import { basicCaseCommentPatch, basicCasePost, basicResolvedCase, - casesStatus, pushedCase, tags, categories, @@ -69,9 +67,6 @@ export const getSingleCaseMetrics = async ( signal: AbortSignal ): Promise<SingleCaseMetricsResponse> => Promise.resolve(basicCaseMetrics); -export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => - Promise.resolve(casesStatus); - export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); export const findAssignees = async (): Promise<UserProfile[]> => userProfiles; diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 02d639b790885..d72eece49e1e3 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { httpServiceMock } from '@kbn/core/public/mocks'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; import { KibanaServices } from '../common/lib/kibana'; @@ -51,13 +50,11 @@ import { basicCaseSnake, pushedCaseSnake, categories, - casesStatus, casesSnake, cases, pushedCase, tags, findCaseUserActionsResponse, - casesStatusSnake, basicCaseId, caseWithRegisteredAttachmentsSnake, caseWithRegisteredAttachments, @@ -69,7 +66,6 @@ import { } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './constants'; -import { getCasesStatus } from '../api'; import { getCaseConnectorsMockResponse } from '../common/mock/connectors'; import { set } from '@kbn/safer-lodash-set'; import { cloneDeep, omit } from 'lodash'; @@ -531,38 +527,6 @@ describe('Cases API', () => { }); }); - describe('getCasesStatus', () => { - const http = httpServiceMock.createStartContract({ basePath: '' }); - http.get.mockResolvedValue(casesStatusSnake); - - beforeEach(() => { - fetchMock.mockClear(); - }); - - it('should be called with correct check url, method, signal', async () => { - await getCasesStatus({ - http, - signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER] }, - }); - - expect(http.get).toHaveBeenCalledWith(`${CASES_URL}/status`, { - signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER] }, - }); - }); - - it('should return correct response', async () => { - const resp = await getCasesStatus({ - http, - signal: abortCtrl.signal, - query: { owner: SECURITY_SOLUTION_OWNER }, - }); - - expect(resp).toEqual(casesStatus); - }); - }); - describe('findCaseUserActions', () => { const findCaseUserActionsSnake = { page: 1, diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx index 5718b07438a98..99d79484b17cc 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; @@ -43,10 +43,10 @@ describe('useActionTypes', () => { (useToasts as jest.Mock).mockReturnValue({ addError: addErrorMock }); - const { waitForNextUpdate } = renderHook(() => useGetActionTypes(), { + renderHook(() => useGetActionTypes(), { wrapper: appMockRenderer.AppWrapper, }); - await waitForNextUpdate({ timeout: 2000 }); - expect(addErrorMock).toHaveBeenCalled(); + + await waitFor(() => expect(addErrorMock).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_all_case_configurations.test.ts b/x-pack/plugins/cases/public/containers/configure/use_get_all_case_configurations.test.ts index cd9e44d1bdaae..52d4df20e5401 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_all_case_configurations.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/use_get_all_case_configurations.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useGetAllCaseConfigurations } from './use_get_all_case_configurations'; import * as api from './api'; import type { AppMockRenderer } from '../../common/mock'; @@ -32,18 +32,11 @@ describe('Use get all case configurations hook', () => { { id: 'my-configuration-3', owner: '3' }, ]); - const { result, waitForNextUpdate } = renderHook(() => useGetAllCaseConfigurations(), { + const { result } = renderHook(() => useGetAllCaseConfigurations(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - /** - * Ensures that the initial data is returned≠ - * before fetching - */ - // @ts-expect-error: data is defined - expect(result.all[0].data).toEqual([ + expect(result.current.data).toEqual([ { closureType: 'close-by-user', connector: { fields: null, id: 'none', name: 'none', type: '.none' }, @@ -56,43 +49,36 @@ describe('Use get all case configurations hook', () => { }, ]); - /** - * The response after fetching - */ - // @ts-expect-error: data is defined - expect(result.all[1].data).toEqual([ - { id: 'my-configuration-1', owner: '1' }, - { id: 'my-configuration-2', owner: '2' }, - { id: 'my-configuration-3', owner: '3' }, - ]); + await waitFor(() => + expect(result.current.data).toEqual([ + { id: 'my-configuration-1', owner: '1' }, + { id: 'my-configuration-2', owner: '2' }, + { id: 'my-configuration-3', owner: '3' }, + ]) + ); }); it('returns the initial configuration if none is available', async () => { const spy = jest.spyOn(api, 'getCaseConfigure'); spy.mockResolvedValue([]); - const { result, waitForNextUpdate } = renderHook(() => useGetAllCaseConfigurations(), { + const { result } = renderHook(() => useGetAllCaseConfigurations(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - /** - * Ensures that the initial data is returned≠ - * before fetching - */ - // @ts-expect-error: data is defined - expect(result.all[0].data).toEqual([ - { - closureType: 'close-by-user', - connector: { fields: null, id: 'none', name: 'none', type: '.none' }, - customFields: [], - templates: [], - id: '', - mappings: [], - version: '', - owner: '', - }, - ]); + await waitFor(() => + expect(result.current.data).toEqual([ + { + closureType: 'close-by-user', + connector: { fields: null, id: 'none', name: 'none', type: '.none' }, + customFields: [], + templates: [], + id: '', + mappings: [], + version: '', + owner: '', + }, + ]) + ); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_case_configuration.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_case_configuration.test.tsx index e504bd22e9cc8..35faebd12a788 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_case_configuration.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_case_configuration.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useGetCaseConfiguration } from './use_get_case_configuration'; import * as api from './api'; import type { AppMockRenderer } from '../../common/mock'; @@ -35,16 +35,14 @@ describe('Use get case configuration hook', () => { targetConfiguration, ]); - const { result, waitForNextUpdate } = renderHook(() => useGetCaseConfiguration(), { + const { result } = renderHook(() => useGetCaseConfiguration(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - /** * The response after fetching */ - expect(result.current.data).toEqual(targetConfiguration); + await waitFor(() => expect(result.current.data).toEqual(targetConfiguration)); }); it('returns the initial configuration if none matches the owner', async () => { @@ -59,16 +57,14 @@ describe('Use get case configuration hook', () => { { ...initialConfiguration, id: 'my-new-configuration-2', owner: 'bar' }, ]); - const { result, waitForNextUpdate } = renderHook(() => useGetCaseConfiguration(), { + const { result } = renderHook(() => useGetCaseConfiguration(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - /** * The response after fetching */ - expect(result.current.data).toEqual(initialConfiguration); + await waitFor(() => expect(result.current.data).toEqual(initialConfiguration)); }); it('returns the initial configuration if none exists', async () => { @@ -76,16 +72,14 @@ describe('Use get case configuration hook', () => { spy.mockResolvedValue([]); - const { result, waitForNextUpdate } = renderHook(() => useGetCaseConfiguration(), { + const { result } = renderHook(() => useGetCaseConfiguration(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - /** * The response after fetching */ - expect(result.current.data).toEqual(initialConfiguration); + await waitFor(() => expect(result.current.data).toEqual(initialConfiguration)); }); it('returns the initial configuration if the owner is undefined', async () => { @@ -94,15 +88,13 @@ describe('Use get case configuration hook', () => { spy.mockResolvedValue([]); - const { result, waitForNextUpdate } = renderHook(() => useGetCaseConfiguration(), { + const { result } = renderHook(() => useGetCaseConfiguration(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - /** * The response after fetching */ - expect(result.current.data).toEqual(initialConfiguration); + await waitFor(() => expect(result.current.data).toEqual(initialConfiguration)); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_case_configurations_query.test.ts b/x-pack/plugins/cases/public/containers/configure/use_get_case_configurations_query.test.ts index adc06ad840d90..02cb834421445 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_case_configurations_query.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/use_get_case_configurations_query.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import { useGetCaseConfigurationsQuery } from './use_get_case_configurations_query'; import * as api from './api'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useToasts } from '../../common/lib/kibana'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx index a81d6ac46d189..c9dad2340f2b8 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { noConnectorsCasePermission, TestProviders } from '../../common/mock'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; @@ -26,15 +26,13 @@ describe('useConnectors', () => { it('fetches connectors', async () => { const spy = jest.spyOn(api, 'getSupportedActionConnectors'); - const { waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { + renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + await waitFor(() => expect(spy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) })); }); it('shows a toast error when the API returns error', async () => { @@ -46,45 +44,44 @@ describe('useConnectors', () => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { + renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); it('does not fetch connectors when the user does not has access to actions', async () => { const spyOnFetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors'); useApplicationCapabilitiesMock().actions = { crud: false, read: false }; - const { result, waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { + const { result } = renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - - expect(spyOnFetchConnectors).not.toHaveBeenCalled(); - expect(result.current.data).toEqual([]); + await waitFor(() => { + expect(spyOnFetchConnectors).not.toHaveBeenCalled(); + expect(result.current.data).toEqual([]); + }); }); it('does not fetch connectors when the user does not has access to connectors', async () => { const spyOnFetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors'); useApplicationCapabilitiesMock().actions = { crud: true, read: true }; - const { result, waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { + const { result } = renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders permissions={noConnectorsCasePermission()}>{children}</TestProviders> ), }); - await waitForNextUpdate(); - - expect(spyOnFetchConnectors).not.toHaveBeenCalled(); - expect(result.current.data).toEqual([]); + await waitFor(() => { + expect(spyOnFetchConnectors).not.toHaveBeenCalled(); + expect(result.current.data).toEqual([]); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx index 4fab35fd5ce5f..04b266478f667 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { usePersistConfiguration } from './use_persist_configuration'; import * as api from './api'; @@ -55,7 +55,7 @@ describe('usePersistConfiguration', () => { const spyPost = jest.spyOn(api, 'postCaseConfigure'); const spyPatch = jest.spyOn(api, 'patchCaseConfigure'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -80,7 +80,7 @@ describe('usePersistConfiguration', () => { const spyPost = jest.spyOn(api, 'postCaseConfigure'); const spyPatch = jest.spyOn(api, 'patchCaseConfigure'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -104,7 +104,7 @@ describe('usePersistConfiguration', () => { it('calls postCaseConfigure with correct data', async () => { const spyPost = jest.spyOn(api, 'postCaseConfigure'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -133,7 +133,7 @@ describe('usePersistConfiguration', () => { const spyPost = jest.spyOn(api, 'postCaseConfigure'); const spyPatch = jest.spyOn(api, 'patchCaseConfigure'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -157,7 +157,7 @@ describe('usePersistConfiguration', () => { it('calls patchCaseConfigure with correct data', async () => { const spyPatch = jest.spyOn(api, 'patchCaseConfigure'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -184,7 +184,7 @@ describe('usePersistConfiguration', () => { it('invalidates the queries correctly', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -198,7 +198,7 @@ describe('usePersistConfiguration', () => { }); it('shows the success toaster', async () => { - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); @@ -216,7 +216,7 @@ describe('usePersistConfiguration', () => { .spyOn(api, 'postCaseConfigure') .mockRejectedValue(new Error('useCreateAttachments: Test error')); - const { waitFor, result } = renderHook(() => usePersistConfiguration(), { + const { result } = renderHook(() => usePersistConfiguration(), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index c3cee2d60d2b0..1ed160cf1847b 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -27,7 +27,7 @@ import { ExternalReferenceStorageType, CustomFieldTypes, } from '../../common/types/domain'; -import type { ActionLicense, CaseUI, CasesStatus, UserActionUI } from './types'; +import type { ActionLicense, CaseUI, UserActionUI } from './types'; import type { ResolvedCase, @@ -56,11 +56,7 @@ import type { AttachmentViewObject, PersistableStateAttachmentType, } from '../client/attachment_framework/types'; -import type { - CasesFindResponse, - CasesStatusResponse, - UserActionWithResponse, -} from '../../common/types/api'; +import type { CasesFindResponse, UserActionWithResponse } from '../../common/types/api'; export { connectorsMock } from '../common/mock/connectors'; export const basicCaseId = 'basic-case-id'; @@ -398,14 +394,13 @@ export const basicCaseCommentPatch = { comments: [basicCommentPatch], }; -export const casesStatus: CasesStatus = { - countOpenCases: 20, - countInProgressCases: 40, - countClosedCases: 130, -}; - export const casesMetrics: CasesMetrics = { mttr: 12, + status: { + open: 20, + inProgress: 40, + closed: 130, + }, }; export const basicPush = { @@ -461,7 +456,9 @@ export const allCases: CasesFindResponseUI = { page: 1, perPage: 5, total: 10, - ...casesStatus, + countOpenCases: 20, + countInProgressCases: 40, + countClosedCases: 130, }; export const actionLicenses: ActionLicense[] = [ @@ -572,12 +569,6 @@ export const caseWithRegisteredAttachmentsSnake = { comments: [externalReferenceAttachmentSnake, persistableStateAttachmentSnake], }; -export const casesStatusSnake: CasesStatusResponse = { - count_closed_cases: 130, - count_in_progress_cases: 40, - count_open_cases: 20, -}; - export const pushSnake = { connector_id: pushConnectorId, connector_name: 'My SN connector', @@ -626,7 +617,9 @@ export const allCasesSnake: CasesFindResponse = { page: 1, per_page: 5, total: 10, - ...casesStatusSnake, + count_closed_cases: 130, + count_in_progress_cases: 40, + count_open_cases: 20, }; export const getUserAction = ( diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx index 17b4568cc9de7..50d28921e1aa1 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useUpdateCases } from './use_bulk_update_case'; import { allCases } from './mock'; import { useToasts } from '../common/lib/kibana'; @@ -32,7 +32,7 @@ describe('useUpdateCases', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'updateCases'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + const { result } = renderHook(() => useUpdateCases(), { wrapper: appMockRender.AppWrapper, }); @@ -40,14 +40,12 @@ describe('useUpdateCases', () => { result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ cases: allCases.cases }); + await waitFor(() => expect(spy).toHaveBeenCalledWith({ cases: allCases.cases })); }); it('invalidates the queries correctly', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + const { result } = renderHook(() => useUpdateCases(), { wrapper: appMockRender.AppWrapper, }); @@ -55,15 +53,15 @@ describe('useUpdateCases', () => { result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + }); }); it('shows a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + const { result } = renderHook(() => useUpdateCases(), { wrapper: appMockRender.AppWrapper, }); @@ -71,18 +69,18 @@ describe('useUpdateCases', () => { result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'Success title', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'Success title', + className: 'eui-textBreakWord', + }) + ); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'updateCases').mockRejectedValue(new Error('useUpdateCases: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + const { result } = renderHook(() => useUpdateCases(), { wrapper: appMockRender.AppWrapper, }); @@ -90,8 +88,6 @@ describe('useUpdateCases', () => { result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx b/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx index 14d4477df62da..c7aa4b521bcda 100644 --- a/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { AttachmentType } from '../../common/types/domain'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; @@ -58,7 +58,7 @@ describe('useCreateAttachments', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'createAttachments'); - const { waitForNextUpdate, result } = renderHook(() => useCreateAttachments(), { + const { result } = renderHook(() => useCreateAttachments(), { wrapper: appMockRender.AppWrapper, }); @@ -66,13 +66,16 @@ describe('useCreateAttachments', () => { result.current.mutate(request); }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ attachments: attachmentsWithOwner, caseId: request.caseId }); + await waitFor(() => + expect(spy).toHaveBeenCalledWith({ + attachments: attachmentsWithOwner, + caseId: request.caseId, + }) + ); }); it('does not show a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => useCreateAttachments(), { + const { result } = renderHook(() => useCreateAttachments(), { wrapper: appMockRender.AppWrapper, }); @@ -80,9 +83,7 @@ describe('useCreateAttachments', () => { result.current.mutate(request); }); - await waitForNextUpdate(); - - expect(addSuccess).not.toHaveBeenCalled(); + await waitFor(() => expect(addSuccess).not.toHaveBeenCalled()); }); it('shows a toast error when the api return an error', async () => { @@ -90,7 +91,7 @@ describe('useCreateAttachments', () => { .spyOn(api, 'createAttachments') .mockRejectedValue(new Error('useCreateAttachments: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useCreateAttachments(), { + const { result } = renderHook(() => useCreateAttachments(), { wrapper: appMockRender.AppWrapper, }); @@ -98,8 +99,6 @@ describe('useCreateAttachments', () => { result.current.mutate(request); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx index f0ec4e9a43d87..405351c3f0681 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useDeleteCases } from './use_delete_cases'; import * as api from './api'; @@ -32,7 +32,7 @@ describe('useDeleteCases', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'deleteCases'); - const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + const { result } = renderHook(() => useDeleteCases(), { wrapper: appMockRender.AppWrapper, }); @@ -40,14 +40,12 @@ describe('useDeleteCases', () => { result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ caseIds: ['1', '2'] }); + await waitFor(() => expect(spy).toHaveBeenCalledWith({ caseIds: ['1', '2'] })); }); it('invalidates the queries correctly', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + const { result } = renderHook(() => useDeleteCases(), { wrapper: appMockRender.AppWrapper, }); @@ -55,15 +53,15 @@ describe('useDeleteCases', () => { result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + }); }); it('shows a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + const { result } = renderHook(() => useDeleteCases(), { wrapper: appMockRender.AppWrapper, }); @@ -71,18 +69,18 @@ describe('useDeleteCases', () => { result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'Success title', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'Success title', + className: 'eui-textBreakWord', + }) + ); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'deleteCases').mockRejectedValue(new Error('useDeleteCases: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + const { result } = renderHook(() => useDeleteCases(), { wrapper: appMockRender.AppWrapper, }); @@ -90,8 +88,6 @@ describe('useDeleteCases', () => { result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_delete_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_comment.test.tsx index c185a7394c887..c3d0739cec370 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_comment.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useDeleteComment } from './use_delete_comment'; import * as api from './api'; import { basicCaseId } from './mock'; @@ -45,7 +45,7 @@ describe('useDeleteComment', () => { it('calls deleteComment with correct arguments - case', async () => { const spyOnDeleteComment = jest.spyOn(api, 'deleteComment'); - const { waitForNextUpdate, result } = renderHook(() => useDeleteComment(), { + const { result } = renderHook(() => useDeleteComment(), { wrapper: appMockRender.AppWrapper, }); @@ -57,32 +57,32 @@ describe('useDeleteComment', () => { }); }); - await waitForNextUpdate(); - - expect(spyOnDeleteComment).toBeCalledWith({ - caseId: basicCaseId, - commentId, - }); + await waitFor(() => + expect(spyOnDeleteComment).toBeCalledWith({ + caseId: basicCaseId, + commentId, + }) + ); }); it('refreshes the case page view after delete', async () => { - const { waitForNextUpdate, result } = renderHook(() => useDeleteComment(), { + const { result } = renderHook(() => useDeleteComment(), { wrapper: appMockRender.AppWrapper, }); - result.current.mutate({ - caseId: basicCaseId, - commentId, - successToasterTitle, + act(() => { + result.current.mutate({ + caseId: basicCaseId, + commentId, + successToasterTitle, + }); }); - await waitForNextUpdate(); - - expect(useRefreshCaseViewPage()).toBeCalled(); + await waitFor(() => expect(useRefreshCaseViewPage()).toBeCalled()); }); it('shows a success toaster correctly', async () => { - const { waitForNextUpdate, result } = renderHook(() => useDeleteComment(), { + const { result } = renderHook(() => useDeleteComment(), { wrapper: appMockRender.AppWrapper, }); @@ -94,36 +94,38 @@ describe('useDeleteComment', () => { }); }); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'Deleted', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'Deleted', + className: 'eui-textBreakWord', + }) + ); }); it('sets isError when fails to delete a case', async () => { const spyOnDeleteComment = jest.spyOn(api, 'deleteComment'); spyOnDeleteComment.mockRejectedValue(new Error('Error')); - const { waitForNextUpdate, result } = renderHook(() => useDeleteComment(), { + const { result } = renderHook(() => useDeleteComment(), { wrapper: appMockRender.AppWrapper, }); - result.current.mutate({ - caseId: basicCaseId, - commentId, - successToasterTitle, + act(() => { + result.current.mutate({ + caseId: basicCaseId, + commentId, + successToasterTitle, + }); }); - await waitForNextUpdate(); + await waitFor(() => { + expect(spyOnDeleteComment).toBeCalledWith({ + caseId: basicCaseId, + commentId, + }); - expect(spyOnDeleteComment).toBeCalledWith({ - caseId: basicCaseId, - commentId, + expect(addError).toHaveBeenCalled(); + expect(result.current.isError).toBe(true); }); - - expect(addError).toHaveBeenCalled(); - expect(result.current.isError).toBe(true); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx index 51382a0f548da..329423e41dfaa 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_file_attachment.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { basicCaseId, basicFileMock } from './mock'; import { useRefreshCaseViewPage } from '../components/case_view/use_on_refresh_case_view_page'; @@ -34,7 +34,7 @@ describe('useDeleteFileAttachment', () => { it('calls deleteFileAttachment with correct arguments - case', async () => { const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); - const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + const { result } = renderHook(() => useDeleteFileAttachment(), { wrapper: appMockRender.AppWrapper, }); @@ -45,16 +45,16 @@ describe('useDeleteFileAttachment', () => { }); }); - await waitForNextUpdate(); - - expect(spyOnDeleteFileAttachments).toHaveBeenCalledWith({ - caseId: basicCaseId, - fileIds: [basicFileMock.id], - }); + await waitFor(() => + expect(spyOnDeleteFileAttachments).toHaveBeenCalledWith({ + caseId: basicCaseId, + fileIds: [basicFileMock.id], + }) + ); }); it('refreshes the case page view', async () => { - const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + const { result } = renderHook(() => useDeleteFileAttachment(), { wrapper: appMockRender.AppWrapper, }); @@ -65,13 +65,11 @@ describe('useDeleteFileAttachment', () => { }) ); - await waitForNextUpdate(); - - expect(useRefreshCaseViewPage()).toBeCalled(); + await waitFor(() => expect(useRefreshCaseViewPage()).toBeCalled()); }); it('shows a success toaster correctly', async () => { - const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + const { result } = renderHook(() => useDeleteFileAttachment(), { wrapper: appMockRender.AppWrapper, }); @@ -82,19 +80,19 @@ describe('useDeleteFileAttachment', () => { }) ); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'File deleted successfully', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'File deleted successfully', + className: 'eui-textBreakWord', + }) + ); }); it('sets isError when fails to delete a file attachment', async () => { const spyOnDeleteFileAttachments = jest.spyOn(api, 'deleteFileAttachments'); spyOnDeleteFileAttachments.mockRejectedValue(new Error('Error')); - const { waitForNextUpdate, result } = renderHook(() => useDeleteFileAttachment(), { + const { result } = renderHook(() => useDeleteFileAttachment(), { wrapper: appMockRender.AppWrapper, }); @@ -105,7 +103,7 @@ describe('useDeleteFileAttachment', () => { }) ); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isError).toBe(true)); expect(spyOnDeleteFileAttachments).toBeCalledWith({ caseId: basicCaseId, @@ -113,6 +111,5 @@ describe('useDeleteFileAttachment', () => { }); expect(addError).toHaveBeenCalled(); - expect(result.current.isError).toBe(true); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx index 67110c8ebc0b9..7dfaa1ff146ee 100644 --- a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useFindCaseUserActions } from './use_find_case_user_actions'; import type { CaseUserActionTypeWithAll } from '../../common/ui/types'; import { basicCase, findCaseUserActionsResponse } from './mock'; @@ -43,33 +43,32 @@ describe('UseFindCaseUserActions', () => { }); it('returns proper state on findCaseUserActions', async () => { - const { result, waitForNextUpdate } = renderHook( - () => useFindCaseUserActions(basicCase.id, params, isEnabled), - { wrapper: appMockRender.AppWrapper } - ); - - await waitForNextUpdate(); - - expect(result.current).toEqual( - expect.objectContaining({ - ...initialData, - data: { - userActions: [...findCaseUserActionsResponse.userActions], - total: 30, - perPage: 10, - page: 1, - }, - isError: false, - isLoading: false, - isFetching: false, - }) + const { result } = renderHook(() => useFindCaseUserActions(basicCase.id, params, isEnabled), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => + expect(result.current).toEqual( + expect.objectContaining({ + ...initialData, + data: { + userActions: [...findCaseUserActionsResponse.userActions], + total: 30, + perPage: 10, + page: 1, + }, + isError: false, + isLoading: false, + isFetching: false, + }) + ) ); }); it('calls the API with correct parameters', async () => { const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData); - const { waitForNextUpdate } = renderHook( + renderHook( () => useFindCaseUserActions( basicCase.id, @@ -84,12 +83,12 @@ describe('UseFindCaseUserActions', () => { { wrapper: appMockRender.AppWrapper } ); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith( - basicCase.id, - { type: 'user', sortOrder: 'desc', page: 1, perPage: 5 }, - expect.any(AbortSignal) + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + basicCase.id, + { type: 'user', sortOrder: 'desc', page: 1, perPage: 5 }, + expect.any(AbortSignal) + ) ); }); @@ -120,20 +119,17 @@ describe('UseFindCaseUserActions', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); - const { waitForNextUpdate } = renderHook( - () => useFindCaseUserActions(basicCase.id, params, isEnabled), - { - wrapper: appMockRender.AppWrapper, - } - ); - - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith( - basicCase.id, - { type: filterActionType, sortOrder, page: 1, perPage: 10 }, - expect.any(AbortSignal) - ); - expect(addError).toHaveBeenCalled(); + renderHook(() => useFindCaseUserActions(basicCase.id, params, isEnabled), { + wrapper: appMockRender.AppWrapper, + }); + + await waitFor(() => { + expect(spy).toHaveBeenCalledWith( + basicCase.id, + { type: filterActionType, sortOrder, page: 1, perPage: 10 }, + expect.any(AbortSignal) + ); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx index 43e445fa39aab..33cba9ef71235 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { useGetActionLicense } from './use_get_action_license'; import type { AppMockRenderer } from '../common/mock'; @@ -25,12 +25,11 @@ describe('useGetActionLicense', () => { it('calls getActionLicense with correct arguments', async () => { const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); - const { waitForNextUpdate } = renderHook(() => useGetActionLicense(), { + renderHook(() => useGetActionLicense(), { wrapper: appMockRenderer.AppWrapper, }); - await waitForNextUpdate(); - expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal); + await waitFor(() => expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal)); }); it('unhappy path', async () => { @@ -42,11 +41,9 @@ describe('useGetActionLicense', () => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetActionLicense(), { + renderHook(() => useGetActionLicense(), { wrapper: appMockRenderer.AppWrapper, }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index 1187c7de07d28..6f693e5d4dc18 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import { useGetCase } from './use_get_case'; import * as api from './api'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -32,21 +31,21 @@ const wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => { describe.skip('Use get case hook', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'resolveCase'); - const { waitForNextUpdate } = renderHook(() => useGetCase('case-1'), { wrapper }); - await waitForNextUpdate(); - expect(spy).toHaveBeenCalledWith({ - caseId: 'case-1', - includeComments: true, - signal: expect.any(AbortSignal), - }); + renderHook(() => useGetCase('case-1'), { wrapper }); + await waitFor(() => + expect(spy).toHaveBeenCalledWith({ + caseId: 'case-1', + includeComments: true, + signal: expect.any(AbortSignal), + }) + ); }); it('shows a toast error when the api return an error', async () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); const spy = jest.spyOn(api, 'resolveCase').mockRejectedValue(new Error("C'est la vie")); - const { waitForNextUpdate } = renderHook(() => useGetCase('case-1'), { wrapper }); - await waitForNextUpdate(); + renderHook(() => useGetCase('case-1'), { wrapper }); await waitFor(() => { expect(spy).toHaveBeenCalledWith({ caseId: 'case-1', diff --git a/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx index e73012f83f729..713f8865cd020 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import type { AppMockRenderer } from '../common/mock'; import { createAppMockRenderer } from '../common/mock'; @@ -30,7 +30,7 @@ describe('useGetCaseConnectors', () => { it('calls getCaseConnectors with correct arguments', async () => { const spyOnGetCases = jest.spyOn(api, 'getCaseConnectors'); - const { waitFor } = renderHook(() => useGetCaseConnectors(caseId), { + renderHook(() => useGetCaseConnectors(caseId), { wrapper: appMockRender.AppWrapper, }); @@ -50,7 +50,7 @@ describe('useGetCaseConnectors', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { waitFor } = renderHook(() => useGetCaseConnectors(caseId), { + renderHook(() => useGetCaseConnectors(caseId), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx index e7c55cd1fc0c5..dbf800577e850 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_file_stats.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { basicCase } from './mock'; @@ -37,12 +37,12 @@ describe('useGetCaseFileStats', () => { }); it('calls filesClient.list with correct arguments', async () => { - const { waitForNextUpdate } = renderHook(() => useGetCaseFileStats(hookParams), { + renderHook(() => useGetCaseFileStats(hookParams), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(appMockRender.getFilesClient().list).toHaveBeenCalledWith(expectedCallParams); + await waitFor(() => + expect(appMockRender.getFilesClient().list).toHaveBeenCalledWith(expectedCallParams) + ); }); it('shows an error toast when filesClient.list throws', async () => { @@ -53,12 +53,12 @@ describe('useGetCaseFileStats', () => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetCaseFileStats(hookParams), { + renderHook(() => useGetCaseFileStats(hookParams), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(appMockRender.getFilesClient().list).toHaveBeenCalledWith(expectedCallParams); - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(appMockRender.getFilesClient().list).toHaveBeenCalledWith(expectedCallParams); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx index 2be8dd6052408..c95eaf15aefe0 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_files.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { basicCase } from './mock'; @@ -48,22 +48,22 @@ describe('useGetCaseFiles', () => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetCaseFiles(hookParams), { + renderHook(() => useGetCaseFiles(hookParams), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(appMockRender.getFilesClient().list).toBeCalledWith(expectedCallParams); - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(appMockRender.getFilesClient().list).toBeCalledWith(expectedCallParams); + expect(addError).toHaveBeenCalled(); + }); }); it('calls filesClient.list with correct arguments', async () => { - const { waitForNextUpdate } = renderHook(() => useGetCaseFiles(hookParams), { + renderHook(() => useGetCaseFiles(hookParams), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(appMockRender.getFilesClient().list).toBeCalledWith(expectedCallParams); + await waitFor(() => + expect(appMockRender.getFilesClient().list).toBeCalledWith(expectedCallParams) + ); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx index 3cda384f1c63c..0feb032649a65 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx @@ -7,7 +7,7 @@ import type { FC, PropsWithChildren } from 'react'; import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import type { SingleCaseMetricsFeature } from '../../common/ui'; import { useGetCaseMetrics } from './use_get_case_metrics'; import { basicCase } from './mock'; @@ -34,12 +34,13 @@ describe('useGetCaseMetrics', () => { it('calls getSingleCaseMetrics with correct arguments', async () => { const spyOnGetCaseMetrics = jest.spyOn(api, 'getSingleCaseMetrics'); - const { waitForNextUpdate } = renderHook(() => useGetCaseMetrics(basicCase.id, features), { + renderHook(() => useGetCaseMetrics(basicCase.id, features), { wrapper, }); - await waitForNextUpdate(); - expect(spyOnGetCaseMetrics).toBeCalledWith(basicCase.id, features, abortCtrl.signal); + await waitFor(() => + expect(spyOnGetCaseMetrics).toBeCalledWith(basicCase.id, features, abortCtrl.signal) + ); }); it('shows an error toast when getSingleCaseMetrics throws', async () => { @@ -51,13 +52,13 @@ describe('useGetCaseMetrics', () => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetCaseMetrics(basicCase.id, features), { + renderHook(() => useGetCaseMetrics(basicCase.id, features), { wrapper, }); - await waitForNextUpdate(); - - expect(spyOnGetCaseMetrics).toBeCalledWith(basicCase.id, features, abortCtrl.signal); - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(spyOnGetCaseMetrics).toBeCalledWith(basicCase.id, features, abortCtrl.signal); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx index b81f9d1448aa2..bd8ecf7393b2a 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; + +import { waitFor, renderHook } from '@testing-library/react'; import { useToasts } from '../common/lib/kibana'; import type { AppMockRenderer } from '../common/mock'; @@ -31,12 +32,11 @@ describe('useGetCaseUserActionsStats', () => { }); it('returns proper state on getCaseUserActionsStats', async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCaseUserActionsStats(basicCase.id), - { wrapper: appMockRender.AppWrapper } - ); + const { result } = renderHook(() => useGetCaseUserActionsStats(basicCase.id), { + wrapper: appMockRender.AppWrapper, + }); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current).toEqual( expect.objectContaining({ @@ -61,25 +61,23 @@ describe('useGetCaseUserActionsStats', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); - const { waitForNextUpdate } = renderHook(() => useGetCaseUserActionsStats(basicCase.id), { + renderHook(() => useGetCaseUserActionsStats(basicCase.id), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal)); - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal)); + expect(addError).toHaveBeenCalled(); + }); }); it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getCaseUserActionsStats'); - const { waitForNextUpdate } = renderHook(() => useGetCaseUserActionsStats(basicCase.id), { + renderHook(() => useGetCaseUserActionsStats(basicCase.id), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal)); + await waitFor(() => expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal))); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_users.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_users.test.tsx index e35e41c3d49f6..7f425081989b8 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_users.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_users.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useGetCaseUsers } from './use_get_case_users'; import * as api from './api'; import { useToasts } from '../common/lib/kibana'; @@ -26,13 +26,13 @@ describe('useGetCaseUsers', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getCaseUsers'); - const { waitForNextUpdate } = renderHook(() => useGetCaseUsers('case-1'), { + renderHook(() => useGetCaseUsers('case-1'), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ caseId: 'case-1', signal: expect.any(AbortSignal) }); + await waitFor(() => + expect(spy).toHaveBeenCalledWith({ caseId: 'case-1', signal: expect.any(AbortSignal) }) + ); }); it('shows a toast error when the api return an error', async () => { @@ -40,13 +40,13 @@ describe('useGetCaseUsers', () => { (useToasts as jest.Mock).mockReturnValue({ addError }); const spy = jest.spyOn(api, 'getCaseUsers').mockRejectedValue(new Error("C'est la vie")); - const { waitForNextUpdate } = renderHook(() => useGetCaseUsers('case-1'), { + renderHook(() => useGetCaseUsers('case-1'), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ caseId: 'case-1', signal: expect.any(AbortSignal) }); - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(spy).toHaveBeenCalledWith({ caseId: 'case-1', signal: expect.any(AbortSignal) }); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index 92d7abde2f9d2..0719c14e9fc4f 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './constants'; import { useGetCases } from './use_get_cases'; import * as api from './api'; @@ -31,7 +31,7 @@ describe('useGetCases', () => { it('calls getCases with correct arguments', async () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); - const { waitFor } = renderHook(() => useGetCases(), { + renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); @@ -55,7 +55,7 @@ describe('useGetCases', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { waitFor } = renderHook(() => useGetCases(), { + renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); @@ -90,7 +90,7 @@ describe('useGetCases', () => { }; const spyOnGetCases = jest.spyOn(api, 'getCases'); - const { waitFor } = renderHook(() => useGetCases(), { + renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); @@ -109,7 +109,7 @@ describe('useGetCases', () => { appMockRender = createAppMockRenderer({ owner: [] }); const spyOnGetCases = jest.spyOn(api, 'getCases'); - const { waitFor } = renderHook(() => useGetCases(), { + renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); @@ -128,7 +128,7 @@ describe('useGetCases', () => { appMockRender = createAppMockRenderer({ owner: ['observability'] }); const spyOnGetCases = jest.spyOn(api, 'getCases'); - const { waitFor } = renderHook(() => useGetCases(), { + renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); @@ -147,7 +147,7 @@ describe('useGetCases', () => { appMockRender = createAppMockRenderer({ owner: ['observability'] }); const spyOnGetCases = jest.spyOn(api, 'getCases'); - const { waitFor } = renderHook(() => useGetCases({ filterOptions: { owner: ['my-owner'] } }), { + renderHook(() => useGetCases({ filterOptions: { owner: ['my-owner'] } }), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx index 619e1ea4e1ebf..e236ccef19bb2 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from '../api'; import type { AppMockRenderer } from '../common/mock'; import { createAppMockRenderer } from '../common/mock'; @@ -33,17 +33,20 @@ describe('useGetCasesMetrics', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getCasesMetrics'); - const { waitForNextUpdate } = renderHook(() => useGetCasesMetrics(), { + renderHook(() => useGetCasesMetrics(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ - http: expect.anything(), - signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER], features: [CaseMetricsFeature.MTTR] }, - }); + await waitFor(() => + expect(spy).toHaveBeenCalledWith({ + http: expect.anything(), + signal: abortCtrl.signal, + query: { + owner: [SECURITY_SOLUTION_OWNER], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], + }, + }) + ); }); it('shows a toast error when the api return an error', async () => { @@ -51,12 +54,12 @@ describe('useGetCasesMetrics', () => { .spyOn(api, 'getCasesMetrics') .mockRejectedValue(new Error('useGetCasesMetrics: Test error')); - const { waitForNextUpdate } = renderHook(() => useGetCasesMetrics(), { + renderHook(() => useGetCasesMetrics(), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx index 1e91a182f63d6..05df55ee36982 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx @@ -26,7 +26,7 @@ export const useGetCasesMetrics = () => { ({ signal }) => getCasesMetrics({ http, - query: { owner, features: [CaseMetricsFeature.MTTR] }, + query: { owner, features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS] }, signal, }), { diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx deleted file mode 100644 index fed7159ac15e3..0000000000000 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx +++ /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 { renderHook } from '@testing-library/react-hooks'; -import { useGetCasesStatus } from './use_get_cases_status'; -import * as api from '../api'; -import type { AppMockRenderer } from '../common/mock'; -import { createAppMockRenderer } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; -import { useToasts } from '../common/lib/kibana'; - -jest.mock('../api'); -jest.mock('../common/lib/kibana'); - -describe('useGetCasesMetrics', () => { - const abortCtrl = new AbortController(); - const addSuccess = jest.fn(); - const addError = jest.fn(); - - (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - - let appMockRender: AppMockRenderer; - - beforeEach(() => { - appMockRender = createAppMockRenderer(); - jest.clearAllMocks(); - }); - - it('calls the api when invoked with the correct parameters', async () => { - const spy = jest.spyOn(api, 'getCasesStatus'); - const { waitForNextUpdate } = renderHook(() => useGetCasesStatus(), { - wrapper: appMockRender.AppWrapper, - }); - - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ - http: expect.anything(), - signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER] }, - }); - }); - - it('shows a toast error when the api return an error', async () => { - jest - .spyOn(api, 'getCasesStatus') - .mockRejectedValue(new Error('useGetCasesMetrics: Test error')); - - const { waitForNextUpdate } = renderHook(() => useGetCasesStatus(), { - wrapper: appMockRender.AppWrapper, - }); - - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx deleted file mode 100644 index 47733508d18ca..0000000000000 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx +++ /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 { useQuery } from '@tanstack/react-query'; -import { useCasesContext } from '../components/cases_context/use_cases_context'; -import * as i18n from './translations'; -import type { CasesStatus } from './types'; -import { useHttp } from '../common/lib/kibana'; -import { getCasesStatus } from '../api'; -import { useCasesToast } from '../common/use_cases_toast'; -import type { ServerError } from '../types'; -import { casesQueriesKeys } from './constants'; - -export const useGetCasesStatus = () => { - const http = useHttp(); - const { owner } = useCasesContext(); - const { showErrorToast } = useCasesToast(); - - return useQuery<CasesStatus, ServerError>( - casesQueriesKeys.casesStatuses(), - ({ signal }) => - getCasesStatus({ - http, - query: { owner }, - signal, - }), - { - onError: (error: ServerError) => { - showErrorToast(error, { title: i18n.ERROR_TITLE }); - }, - } - ); -}; - -export type UseGetCasesStatus = ReturnType<typeof useGetCasesStatus>; diff --git a/x-pack/plugins/cases/public/containers/use_get_categories.test.tsx b/x-pack/plugins/cases/public/containers/use_get_categories.test.tsx index d80f03822ec7a..11b242a3072d0 100644 --- a/x-pack/plugins/cases/public/containers/use_get_categories.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_categories.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { TestProviders } from '../common/mock'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; @@ -24,18 +24,18 @@ describe('useGetCategories', () => { it('calls getCategories api', async () => { const spyOnGetCategories = jest.spyOn(api, 'getCategories'); - const { waitForNextUpdate } = renderHook(() => useGetCategories(), { + renderHook(() => useGetCategories(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - - expect(spyOnGetCategories).toBeCalledWith({ - signal: abortCtrl.signal, - owner: [SECURITY_SOLUTION_OWNER], - }); + await waitFor(() => + expect(spyOnGetCategories).toBeCalledWith({ + signal: abortCtrl.signal, + owner: [SECURITY_SOLUTION_OWNER], + }) + ); }); it('displays an error toast when an error occurs', async () => { @@ -47,14 +47,12 @@ describe('useGetCategories', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); - const { waitForNextUpdate } = renderHook(() => useGetCategories(), { + renderHook(() => useGetCategories(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - - expect(addError).toBeCalled(); + await waitFor(() => expect(addError).toBeCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx b/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx index 9446007e367d5..dee68dff03371 100644 --- a/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_feature_ids.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useToasts } from '../common/lib/kibana'; import type { AppMockRenderer } from '../common/mock'; import { createAppMockRenderer } from '../common/mock'; @@ -32,12 +31,10 @@ describe('useGetFeaturesIds', () => { it('returns the features ids correctly', async () => { const spy = jest.spyOn(api, 'getFeatureIds').mockRejectedValue([]); - const { waitForNextUpdate } = renderHook(() => useGetFeatureIds(['alert-id-1'], true), { + renderHook(() => useGetFeatureIds(['alert-id-1'], true), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - await waitFor(() => { expect(spy).toHaveBeenCalledWith({ query: { @@ -67,12 +64,10 @@ describe('useGetFeaturesIds', () => { .spyOn(api, 'getFeatureIds') .mockRejectedValue(new Error('Something went wrong')); - const { waitForNextUpdate } = renderHook(() => useGetFeatureIds(['alert-id-1'], true), { + renderHook(() => useGetFeatureIds(['alert-id-1'], true), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - await waitFor(() => { expect(spy).toHaveBeenCalledWith({ query: { diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx index a19074ff279ee..315757f065d88 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { TestProviders } from '../common/mock'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; @@ -24,16 +24,17 @@ describe('useGetTags', () => { it('calls getTags api', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); - const { waitForNextUpdate } = renderHook(() => useGetTags(), { + renderHook(() => useGetTags(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - expect(spyOnGetTags).toBeCalledWith({ - owner: [SECURITY_SOLUTION_OWNER], - signal: abortCtrl.signal, - }); + await waitFor(() => + expect(spyOnGetTags).toBeCalledWith({ + owner: [SECURITY_SOLUTION_OWNER], + signal: abortCtrl.signal, + }) + ); }); it('displays and error toast when an error occurs', async () => { @@ -43,12 +44,11 @@ describe('useGetTags', () => { spyOnGetTags.mockImplementation(() => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetTags(), { + renderHook(() => useGetTags(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( <TestProviders>{children}</TestProviders> ), }); - await waitForNextUpdate(); - expect(addError).toBeCalled(); + await waitFor(() => expect(addError).toBeCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_infinite_find_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_infinite_find_case_user_actions.test.tsx index b3df805033ba8..19a7f2b9edc3f 100644 --- a/x-pack/plugins/cases/public/containers/use_infinite_find_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_infinite_find_case_user_actions.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useInfiniteFindCaseUserActions } from './use_infinite_find_case_user_actions'; import type { CaseUserActionTypeWithAll } from '../../common/ui/types'; @@ -42,12 +42,12 @@ describe('UseInfiniteFindCaseUserActions', () => { }); it('returns proper state on findCaseUserActions', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled), { wrapper: appMockRender.AppWrapper } ); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current).toEqual( expect.objectContaining({ @@ -73,7 +73,7 @@ describe('UseInfiniteFindCaseUserActions', () => { it('calls the API with correct parameters', async () => { const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData); - const { waitForNextUpdate } = renderHook( + renderHook( () => useInfiniteFindCaseUserActions( basicCase.id, @@ -87,12 +87,12 @@ describe('UseInfiniteFindCaseUserActions', () => { { wrapper: appMockRender.AppWrapper } ); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith( - basicCase.id, - { type: 'user', sortOrder: 'desc', page: 1, perPage: 5 }, - expect.any(AbortSignal) + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + basicCase.id, + { type: 'user', sortOrder: 'desc', page: 1, perPage: 5 }, + expect.any(AbortSignal) + ) ); }); @@ -122,33 +122,31 @@ describe('UseInfiniteFindCaseUserActions', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); - const { waitForNextUpdate } = renderHook( - () => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled), - { - wrapper: appMockRender.AppWrapper, - } - ); + renderHook(() => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled), { + wrapper: appMockRender.AppWrapper, + }); - await waitForNextUpdate(); + await waitFor(() => { + expect(spy).toHaveBeenCalledWith( + basicCase.id, + { type: filterActionType, sortOrder, page: 1, perPage: 10 }, + expect.any(AbortSignal) + ); + expect(addError).toHaveBeenCalled(); + }); - expect(spy).toHaveBeenCalledWith( - basicCase.id, - { type: filterActionType, sortOrder, page: 1, perPage: 10 }, - expect.any(AbortSignal) - ); - expect(addError).toHaveBeenCalled(); spy.mockRestore(); }); it('fetches next page with correct params', async () => { const spy = jest.spyOn(api, 'findCaseUserActions'); - const { result, waitFor } = renderHook( + const { result } = renderHook( () => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled), { wrapper: appMockRender.AppWrapper } ); - await waitFor(() => result.current.isSuccess); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.pages).toStrictEqual([findCaseUserActionsResponse]); @@ -165,7 +163,7 @@ describe('UseInfiniteFindCaseUserActions', () => { expect.any(AbortSignal) ); }); - await waitFor(() => result.current.data?.pages.length === 2); + await waitFor(() => expect(result.current.data?.pages).toHaveLength(2)); }); it('returns hasNextPage correctly', async () => { diff --git a/x-pack/plugins/cases/public/containers/use_messages_storage.test.tsx b/x-pack/plugins/cases/public/containers/use_messages_storage.test.tsx index ef4a164e759ba..30254f02412f8 100644 --- a/x-pack/plugins/cases/public/containers/use_messages_storage.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_messages_storage.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import type { UseMessagesStorage } from './use_messages_storage'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useMessagesStorage } from './use_messages_storage'; describe('useMessagesStorage', () => { @@ -18,35 +17,29 @@ describe('useMessagesStorage', () => { }); it('should return an empty array when there is no messages', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getMessages } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { getMessages } = result.current; expect(getMessages('case')).toEqual([]); }); }); it('should add a message', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getMessages, addMessage } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { getMessages, addMessage } = result.current; addMessage('case', 'id-1'); expect(getMessages('case')).toEqual(['id-1']); }); }); it('should add multiple messages', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getMessages, addMessage } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { getMessages, addMessage } = result.current; addMessage('case', 'id-1'); addMessage('case', 'id-2'); expect(getMessages('case')).toEqual(['id-1', 'id-2']); @@ -54,12 +47,10 @@ describe('useMessagesStorage', () => { }); it('should remove a message', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getMessages, addMessage, removeMessage } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { getMessages, addMessage, removeMessage } = result.current; addMessage('case', 'id-1'); addMessage('case', 'id-2'); removeMessage('case', 'id-2'); @@ -68,12 +59,10 @@ describe('useMessagesStorage', () => { }); it('should return presence of a message', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { hasMessage, addMessage, removeMessage } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { hasMessage, addMessage, removeMessage } = result.current; addMessage('case', 'id-1'); addMessage('case', 'id-2'); removeMessage('case', 'id-2'); @@ -83,16 +72,14 @@ describe('useMessagesStorage', () => { }); it('should clear all messages', async () => { + const { result } = renderHook(() => useMessagesStorage()); + await waitFor(() => new Promise((resolve) => resolve(null))); + const { getMessages, addMessage, clearAllMessages } = result.current; await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() => - useMessagesStorage() - ); - await waitForNextUpdate(); - const { getMessages, addMessage, clearAllMessages } = result.current; addMessage('case', 'id-1'); addMessage('case', 'id-2'); clearAllMessages('case'); - expect(getMessages('case')).toEqual([]); }); + expect(getMessages('case')).toEqual([]); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx index 58a878b32e578..d2c4cb65435c1 100644 --- a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import * as api from './api'; import { ConnectorTypes } from '../../common/types/domain'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; @@ -49,7 +49,7 @@ describe('usePostCase', () => { it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'postCase'); - const { waitForNextUpdate, result } = renderHook(() => usePostCase(), { + const { result } = renderHook(() => usePostCase(), { wrapper: appMockRender.AppWrapper, }); @@ -57,14 +57,12 @@ describe('usePostCase', () => { result.current.mutate({ request: samplePost }); }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ newCase: samplePost }); + await waitFor(() => expect(spy).toHaveBeenCalledWith({ newCase: samplePost })); }); it('invalidates the queries correctly', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => usePostCase(), { + const { result } = renderHook(() => usePostCase(), { wrapper: appMockRender.AppWrapper, }); @@ -72,15 +70,15 @@ describe('usePostCase', () => { result.current.mutate({ request: samplePost }); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + }); }); it('does not show a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => usePostCase(), { + const { result } = renderHook(() => usePostCase(), { wrapper: appMockRender.AppWrapper, }); @@ -88,15 +86,13 @@ describe('usePostCase', () => { result.current.mutate({ request: samplePost }); }); - await waitForNextUpdate(); - - expect(addSuccess).not.toHaveBeenCalled(); + await waitFor(() => expect(addSuccess).not.toHaveBeenCalled()); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'postCase').mockRejectedValue(new Error('usePostCase: Test error')); - const { waitForNextUpdate, result } = renderHook(() => usePostCase(), { + const { result } = renderHook(() => usePostCase(), { wrapper: appMockRender.AppWrapper, }); @@ -104,8 +100,6 @@ describe('usePostCase', () => { result.current.mutate({ request: samplePost }); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx index 1d802b3737c96..0c7a76bc6ff3a 100644 --- a/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useToasts } from '../common/lib/kibana'; import { usePostPushToService } from './use_post_push_to_service'; import { pushedCase } from './mock'; @@ -42,7 +42,7 @@ describe('usePostPushToService', () => { it('refresh the case after pushing', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => usePostPushToService(), { + const { result } = renderHook(() => usePostPushToService(), { wrapper: appMockRender.AppWrapper, }); @@ -50,15 +50,15 @@ describe('usePostPushToService', () => { result.current.mutate({ caseId, connector }); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + }); }); it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'pushCase'); - const { waitForNextUpdate, result } = renderHook(() => usePostPushToService(), { + const { result } = renderHook(() => usePostPushToService(), { wrapper: appMockRender.AppWrapper, }); @@ -66,13 +66,11 @@ describe('usePostPushToService', () => { result.current.mutate({ caseId, connector }); }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith({ caseId, connectorId: connector.id }); + await waitFor(() => expect(spy).toHaveBeenCalledWith({ caseId, connectorId: connector.id })); }); it('shows a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => usePostPushToService(), { + const { result } = renderHook(() => usePostPushToService(), { wrapper: appMockRender.AppWrapper, }); @@ -80,18 +78,18 @@ describe('usePostPushToService', () => { result.current.mutate({ caseId, connector }); }); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'Successfully sent to My connector', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'Successfully sent to My connector', + className: 'eui-textBreakWord', + }) + ); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'pushCase').mockRejectedValue(new Error('usePostPushToService: Test error')); - const { waitForNextUpdate, result } = renderHook(() => usePostPushToService(), { + const { result } = renderHook(() => usePostPushToService(), { wrapper: appMockRender.AppWrapper, }); @@ -99,8 +97,6 @@ describe('usePostPushToService', () => { result.current.mutate({ caseId, connector }); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx b/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx index 366d946af1d90..4b17c874339d0 100644 --- a/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { basicCase } from './mock'; import * as api from './api'; import type { AppMockRenderer } from '../common/mock'; @@ -39,7 +39,7 @@ describe('useReplaceCustomField', () => { it('replace a customField and refresh the case page', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + const { result } = renderHook(() => useReplaceCustomField(), { wrapper: appMockRender.AppWrapper, }); @@ -47,15 +47,15 @@ describe('useReplaceCustomField', () => { result.current.mutate(sampleData); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + }); }); it('calls the api when invoked with the correct parameters', async () => { const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); - const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + const { result } = renderHook(() => useReplaceCustomField(), { wrapper: appMockRender.AppWrapper, }); @@ -63,16 +63,16 @@ describe('useReplaceCustomField', () => { result.current.mutate(sampleData); }); - await waitForNextUpdate(); - - expect(patchCustomFieldSpy).toHaveBeenCalledWith({ - caseId: sampleData.caseId, - customFieldId: sampleData.customFieldId, - request: { - value: sampleData.customFieldValue, - caseVersion: sampleData.caseVersion, - }, - }); + await waitFor(() => + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: sampleData.caseId, + customFieldId: sampleData.customFieldId, + request: { + value: sampleData.customFieldValue, + caseVersion: sampleData.caseVersion, + }, + }) + ); }); it('calls the api when invoked with the correct parameters of toggle field', async () => { @@ -83,7 +83,7 @@ describe('useReplaceCustomField', () => { caseVersion: basicCase.version, }; const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); - const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + const { result } = renderHook(() => useReplaceCustomField(), { wrapper: appMockRender.AppWrapper, }); @@ -91,16 +91,16 @@ describe('useReplaceCustomField', () => { result.current.mutate(newData); }); - await waitForNextUpdate(); - - expect(patchCustomFieldSpy).toHaveBeenCalledWith({ - caseId: newData.caseId, - customFieldId: newData.customFieldId, - request: { - value: newData.customFieldValue, - caseVersion: newData.caseVersion, - }, - }); + await waitFor(() => + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: newData.caseId, + customFieldId: newData.customFieldId, + request: { + value: newData.customFieldValue, + caseVersion: newData.caseVersion, + }, + }) + ); }); it('calls the api when invoked with the correct parameters with null value', async () => { @@ -111,7 +111,7 @@ describe('useReplaceCustomField', () => { caseVersion: basicCase.version, }; const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); - const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + const { result } = renderHook(() => useReplaceCustomField(), { wrapper: appMockRender.AppWrapper, }); @@ -119,16 +119,16 @@ describe('useReplaceCustomField', () => { result.current.mutate(newData); }); - await waitForNextUpdate(); - - expect(patchCustomFieldSpy).toHaveBeenCalledWith({ - caseId: newData.caseId, - customFieldId: newData.customFieldId, - request: { - value: newData.customFieldValue, - caseVersion: newData.caseVersion, - }, - }); + await waitFor(() => + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: newData.caseId, + customFieldId: newData.customFieldId, + request: { + value: newData.customFieldValue, + caseVersion: newData.caseVersion, + }, + }) + ); }); it('shows a toast error when the api return an error', async () => { @@ -136,7 +136,7 @@ describe('useReplaceCustomField', () => { .spyOn(api, 'replaceCustomField') .mockRejectedValue(new Error('useUpdateComment: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + const { result } = renderHook(() => useReplaceCustomField(), { wrapper: appMockRender.AppWrapper, }); @@ -144,8 +144,6 @@ describe('useReplaceCustomField', () => { result.current.mutate(sampleData); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_update_case.test.tsx b/x-pack/plugins/cases/public/containers/use_update_case.test.tsx index f8d4ff82078c1..4b14544460792 100644 --- a/x-pack/plugins/cases/public/containers/use_update_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_case.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useUpdateCase } from './use_update_case'; import { basicCase } from './mock'; import * as api from './api'; @@ -41,7 +41,7 @@ describe('useUpdateCase', () => { it('patch case and refresh the case page', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCase(), { + const { result } = renderHook(() => useUpdateCase(), { wrapper: appMockRender.AppWrapper, }); @@ -49,15 +49,15 @@ describe('useUpdateCase', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + }); }); it('calls the api when invoked with the correct parameters', async () => { const patchCaseSpy = jest.spyOn(api, 'patchCase'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCase(), { + const { result } = renderHook(() => useUpdateCase(), { wrapper: appMockRender.AppWrapper, }); @@ -65,17 +65,17 @@ describe('useUpdateCase', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(patchCaseSpy).toHaveBeenCalledWith({ - caseId: basicCase.id, - updatedCase: { description: 'updated description' }, - version: basicCase.version, - }); + await waitFor(() => + expect(patchCaseSpy).toHaveBeenCalledWith({ + caseId: basicCase.id, + updatedCase: { description: 'updated description' }, + version: basicCase.version, + }) + ); }); it('shows a success toaster', async () => { - const { waitForNextUpdate, result } = renderHook(() => useUpdateCase(), { + const { result } = renderHook(() => useUpdateCase(), { wrapper: appMockRender.AppWrapper, }); @@ -83,18 +83,18 @@ describe('useUpdateCase', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(addSuccess).toHaveBeenCalledWith({ - title: 'Updated "Another horrible breach!!"', - className: 'eui-textBreakWord', - }); + await waitFor(() => + expect(addSuccess).toHaveBeenCalledWith({ + title: 'Updated "Another horrible breach!!"', + className: 'eui-textBreakWord', + }) + ); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'patchCase').mockRejectedValue(new Error('useUpdateCase: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useUpdateCase(), { + const { result } = renderHook(() => useUpdateCase(), { wrapper: appMockRender.AppWrapper, }); @@ -102,8 +102,6 @@ describe('useUpdateCase', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx index 59c06bd545c89..7a2522d6ac8c2 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { basicCase } from './mock'; import * as api from './api'; import type { AppMockRenderer } from '../common/mock'; @@ -39,7 +39,7 @@ describe('useUpdateComment', () => { it('patch case and refresh the case page', async () => { const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateComment(), { + const { result } = renderHook(() => useUpdateComment(), { wrapper: appMockRender.AppWrapper, }); @@ -47,15 +47,15 @@ describe('useUpdateComment', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); - expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + }); }); it('calls the api when invoked with the correct parameters', async () => { const patchCommentSpy = jest.spyOn(api, 'patchComment'); - const { waitForNextUpdate, result } = renderHook(() => useUpdateComment(), { + const { result } = renderHook(() => useUpdateComment(), { wrapper: appMockRender.AppWrapper, }); @@ -63,18 +63,18 @@ describe('useUpdateComment', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(patchCommentSpy).toHaveBeenCalledWith({ - ...sampleUpdate, - owner: 'securitySolution', - }); + await waitFor(() => + expect(patchCommentSpy).toHaveBeenCalledWith({ + ...sampleUpdate, + owner: 'securitySolution', + }) + ); }); it('shows a toast error when the api return an error', async () => { jest.spyOn(api, 'patchComment').mockRejectedValue(new Error('useUpdateComment: Test error')); - const { waitForNextUpdate, result } = renderHook(() => useUpdateComment(), { + const { result } = renderHook(() => useUpdateComment(), { wrapper: appMockRender.AppWrapper, }); @@ -82,8 +82,6 @@ describe('useUpdateComment', () => { result.current.mutate(sampleUpdate); }); - await waitForNextUpdate(); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); }); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts index 3622b66aef006..f808d8d7dd934 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_assignees.test.ts @@ -6,7 +6,7 @@ */ import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { userProfiles, userProfilesMap } from './api.mock'; import { useAssignees } from './use_assignees'; diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts index 4edc105b8d349..4558c7cdd2d36 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { useToasts, useKibana } from '../../common/lib/kibana'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; @@ -41,26 +41,25 @@ describe.skip('useBulkGetUserProfiles', () => { it('calls bulkGetUserProfiles with correct arguments', async () => { const spyOnBulkGetUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles'); - const { result, waitFor } = renderHook(() => useBulkGetUserProfiles(props), { + renderHook(() => useBulkGetUserProfiles(props), { wrapper: appMockRender.AppWrapper, }); - await waitFor(() => result.current.isSuccess); - - expect(spyOnBulkGetUserProfiles).toBeCalledWith({ - ...props, - security: expect.anything(), - }); + await waitFor(() => + expect(spyOnBulkGetUserProfiles).toBeCalledWith({ + ...props, + security: expect.anything(), + }) + ); }); it('returns a mapping with user profiles', async () => { - const { result, waitFor } = renderHook(() => useBulkGetUserProfiles(props), { + const { result } = renderHook(() => useBulkGetUserProfiles(props), { wrapper: appMockRender.AppWrapper, }); - await waitFor(() => result.current.isSuccess); - - expect(result.current.data).toMatchInlineSnapshot(` + await waitFor(() => + expect(result.current.data).toMatchInlineSnapshot(` Map { "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0" => Object { "data": Object {}, @@ -93,7 +92,8 @@ describe.skip('useBulkGetUserProfiles', () => { }, }, } - `); + `) + ); }); it('shows a toast error message when an error occurs in the response', async () => { @@ -106,13 +106,11 @@ describe.skip('useBulkGetUserProfiles', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { result, waitFor } = renderHook(() => useBulkGetUserProfiles(props), { + renderHook(() => useBulkGetUserProfiles(props), { wrapper: appMockRender.AppWrapper, }); - await waitFor(() => result.current.isError); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => expect(addError).toHaveBeenCalled()); }); it('does not call the bulkGetUserProfiles if the array of uids is empty', async () => { diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts index c26a6af826548..918771d5b2187 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useToasts, useKibana } from '../../common/lib/kibana'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import type { AppMockRenderer } from '../../common/mock'; @@ -36,7 +36,7 @@ describe('useGetCurrentUserProfile', () => { it('calls getCurrentUserProfile with correct arguments', async () => { const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile'); - const { waitFor } = renderHook(() => useGetCurrentUserProfile(), { + renderHook(() => useGetCurrentUserProfile(), { wrapper: appMockRender.AppWrapper, }); @@ -59,7 +59,7 @@ describe('useGetCurrentUserProfile', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { waitFor } = renderHook(() => useGetCurrentUserProfile(), { + renderHook(() => useGetCurrentUserProfile(), { wrapper: appMockRender.AppWrapper, }); @@ -78,7 +78,7 @@ describe('useGetCurrentUserProfile', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { waitFor } = renderHook(() => useGetCurrentUserProfile(), { + renderHook(() => useGetCurrentUserProfile(), { wrapper: appMockRender.AppWrapper, }); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts index 7daf1d1d5cf62..67907d6494645 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.test.ts @@ -6,7 +6,7 @@ */ import { GENERAL_CASES_OWNER } from '../../../common/constants'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useToasts } from '../../common/lib/kibana'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; @@ -34,17 +34,18 @@ describe('useSuggestUserProfiles', () => { it('calls suggestUserProfiles with correct arguments', async () => { const spyOnSuggestUserProfiles = jest.spyOn(api, 'suggestUserProfiles'); - const { result, waitFor } = renderHook(() => useSuggestUserProfiles(props), { + const { result } = renderHook(() => useSuggestUserProfiles(props), { wrapper: appMockRender.AppWrapper, }); - await waitFor(() => result.current.isSuccess); - - expect(spyOnSuggestUserProfiles).toBeCalledWith({ - ...props, - size: 10, - http: expect.anything(), - signal: expect.anything(), + await waitFor(() => { + expect(result.current.isSuccess).toBeDefined(); + expect(spyOnSuggestUserProfiles).toBeCalledWith({ + ...props, + size: 10, + http: expect.anything(), + signal: expect.anything(), + }); }); }); @@ -58,12 +59,13 @@ describe('useSuggestUserProfiles', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - const { result, waitFor } = renderHook(() => useSuggestUserProfiles(props), { + const { result } = renderHook(() => useSuggestUserProfiles(props), { wrapper: appMockRender.AppWrapper, }); - await waitFor(() => result.current.isError); - - expect(addError).toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.isError).toBeDefined(); + expect(addError).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index 3de6a96979065..a4f1150b7e1ee 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -13,7 +13,6 @@ const apiMock: jest.Mocked<CasesPublicStart['api']> = { cases: { find: jest.fn(), getCasesMetrics: jest.fn(), - getCasesStatus: jest.fn(), bulkGet: jest.fn(), }, }; diff --git a/x-pack/plugins/cases/public/plugin.test.ts b/x-pack/plugins/cases/public/plugin.test.ts index 54f9cf070df44..0d519f038e3b8 100644 --- a/x-pack/plugins/cases/public/plugin.test.ts +++ b/x-pack/plugins/cases/public/plugin.test.ts @@ -129,7 +129,6 @@ describe('Cases Ui Plugin', () => { bulkGet: expect.any(Function), find: expect.any(Function), getCasesMetrics: expect.any(Function), - getCasesStatus: expect.any(Function), }, getRelatedCases: expect.any(Function), }, diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 7f1d4863eac7e..b619919525765 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -40,7 +40,7 @@ import type { GetCasesContextProps } from './client/ui/get_cases_context'; import type { GetCasesProps } from './client/ui/get_cases'; import type { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; import type { GetRecentCasesProps } from './client/ui/get_recent_cases'; -import type { CasesStatus, CasesMetrics, CasesFindResponseUI } from '../common/ui'; +import type { CasesMetrics, CasesFindResponseUI } from '../common/ui'; import type { GroupAlertsByRule } from './client/helpers/group_alerts_by_rule'; import type { getUICapabilities } from './client/helpers/capabilities'; import type { AttachmentFramework } from './client/attachment_framework/types'; @@ -50,7 +50,6 @@ import type { CasesByAlertIDRequest, GetRelatedCasesByAlertResponse, CasesFindRequest, - CasesStatusRequest, CasesBulkGetRequest, CasesBulkGetResponse, CasesMetricsRequest, @@ -125,7 +124,6 @@ export interface CasesPublicStart { ) => Promise<GetRelatedCasesByAlertResponse>; cases: { find: (query: CasesFindRequest, signal?: AbortSignal) => Promise<CasesFindResponseUI>; - getCasesStatus: (query: CasesStatusRequest, signal?: AbortSignal) => Promise<CasesStatus>; getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal) => Promise<CasesMetrics>; bulkGet: (params: CasesBulkGetRequest, signal?: AbortSignal) => Promise<CasesBulkGetResponse>; }; diff --git a/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts b/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts index 755084d624b9f..5cdd4c943b944 100644 --- a/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts @@ -1512,6 +1512,59 @@ describe('update', () => { ); }); + it('throws an error if the case is not found', async () => { + clientArgsMock.services.caseService.getCases.mockResolvedValue({ saved_objects: [] }); + + await expect( + bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgsMock, + casesClientMock + ) + ).rejects.toThrow( + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.' + ); + }); + + it('throws an error if the case is not found and the SO clients returns an SO object', async () => { + clientArgsMock.services.caseService.getCases.mockResolvedValue({ + saved_objects: [ + { + type: 'cases', + id: 'mock-id-1', + references: [], + error: { error: 'Non found', message: 'Non found', statusCode: 404 }, + }, + ], + }); + + await expect( + bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgsMock, + casesClientMock + ) + ).rejects.toThrow( + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.' + ); + }); + describe('Validate max user actions per page', () => { beforeEach(() => { jest.clearAllMocks(); @@ -1681,6 +1734,7 @@ describe('update', () => { status: CaseStatuses.closed, }, }; + clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); clientArgs.services.caseService.patchCases.mockResolvedValue({ @@ -1701,7 +1755,10 @@ describe('update', () => { casesClientMock ); - expect(clientArgs.authorization.ensureAuthorized).not.toThrow(); + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({ + entities: [{ id: closedCase.id, owner: closedCase.attributes.owner }], + operation: [Operations.reopenCase, Operations.updateCase], + }); }); it('throws when user is not authorized to update case', async () => { @@ -1726,38 +1783,6 @@ describe('update', () => { `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized"` ); }); - - it('throws when user is not authorized to reopen case', async () => { - const closedCase = { - ...mockCases[0], - attributes: { - ...mockCases[0].attributes, - status: CaseStatuses.closed, - }, - }; - clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); - - const error = new Error('Unauthorized to reopen case'); - clientArgs.authorization.ensureAuthorized.mockRejectedValueOnce(error); // Reject reopenCase - - await expect( - bulkUpdate( - { - cases: [ - { - id: closedCase.id, - version: closedCase.version ?? '', - status: CaseStatuses.open, - }, - ], - }, - clientArgs, - casesClientMock - ) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized to reopen case"` - ); - }); }); }); }); diff --git a/x-pack/plugins/cases/server/client/cases/bulk_update.ts b/x-pack/plugins/cases/server/client/cases/bulk_update.ts index 9a90168b858de..1f9dd5a7cb28c 100644 --- a/x-pack/plugins/cases/server/client/cases/bulk_update.ts +++ b/x-pack/plugins/cases/server/client/cases/bulk_update.ts @@ -295,6 +295,7 @@ function partitionPatchRequest( ) { // Track cases that are closed and a user is attempting to reopen reopenedCases.push(reqCase); + casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } else { casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.test.ts b/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.test.ts new file mode 100644 index 0000000000000..51d41eac1db48 --- /dev/null +++ b/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasePersistedStatus } from '../../../../common/types/case'; +import { StatusCounts } from './status_counts'; + +describe('StatusCounts', () => { + it('returns the correct aggregation', () => { + const agg = new StatusCounts(); + + expect(agg.build()).toEqual({ + status: { + terms: { + field: 'cases.attributes.status', + size: 3, + }, + }, + }); + }); + + it('formats the response correctly', async () => { + const agg = new StatusCounts(); + const res = agg.formatResponse({ + status: { + buckets: [ + { key: CasePersistedStatus.OPEN, doc_count: 2 }, + { key: CasePersistedStatus.IN_PROGRESS, doc_count: 1 }, + { key: CasePersistedStatus.CLOSED, doc_count: 3 }, + ], + }, + }); + expect(res).toEqual({ status: { open: 2, inProgress: 1, closed: 3 } }); + }); + + it('formats the response correctly if the res is undefined', () => { + const agg = new StatusCounts(); + // @ts-expect-error: testing for undefined response + const res = agg.formatResponse(); + expect(res).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('formats the response correctly if the mttr is not defined', () => { + const agg = new StatusCounts(); + const res = agg.formatResponse({}); + expect(res).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('formats the response correctly if the value is not defined', () => { + const agg = new StatusCounts(); + const res = agg.formatResponse({ status: {} }); + expect(res).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('gets the name correctly', () => { + const agg = new StatusCounts(); + expect(agg.getName()).toBe('status'); + }); +}); diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.ts b/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.ts new file mode 100644 index 0000000000000..f08bf8ac77500 --- /dev/null +++ b/x-pack/plugins/cases/server/client/metrics/all_cases/aggregations/status_counts.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasePersistedStatus } from '../../../../common/types/case'; +import { caseStatuses } from '../../../../../common/types/domain'; +import { CASE_SAVED_OBJECT } from '../../../../../common/constants'; +import type { CasesMetricsResponse } from '../../../../../common/types/api'; +import type { AggregationBuilder, AggregationResponse } from '../../types'; + +export class StatusCounts implements AggregationBuilder<CasesMetricsResponse> { + build() { + return { + status: { + terms: { + field: `${CASE_SAVED_OBJECT}.attributes.status`, + size: caseStatuses.length, + }, + }, + }; + } + + formatResponse(aggregations: AggregationResponse) { + const aggs = aggregations as StatusAggregate; + const buckets = aggs?.status?.buckets ?? []; + const status: Partial<Record<CasePersistedStatus, number>> = {}; + + for (const bucket of buckets) { + status[bucket.key] = bucket.doc_count; + } + + return { + status: { + open: status[CasePersistedStatus.OPEN] ?? 0, + inProgress: status[CasePersistedStatus.IN_PROGRESS] ?? 0, + closed: status[CasePersistedStatus.CLOSED] ?? 0, + }, + }; + } + + getName() { + return 'status'; + } +} + +type StatusAggregate = StatusAggregateResponse | undefined; + +interface StatusAggregateResponse { + status: { + buckets: Array<{ + key: CasePersistedStatus; + doc_count: number; + }>; + }; +} diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases/status.test.ts b/x-pack/plugins/cases/server/client/metrics/all_cases/status.test.ts new file mode 100644 index 0000000000000..ea49af2efee8d --- /dev/null +++ b/x-pack/plugins/cases/server/client/metrics/all_cases/status.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { Case } from '../../../../common/types/domain'; +import { CaseMetricsFeature } from '../../../../common/types/api'; +import { createCasesClientMock } from '../../mocks'; +import type { CasesClientArgs } from '../../types'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { createCaseServiceMock } from '../../../services/mocks'; +import { Status } from './status'; +import { CasePersistedStatus } from '../../../common/types/case'; + +const clientMock = createCasesClientMock(); +const caseService = createCaseServiceMock(); + +const logger = loggingSystemMock.createLogger(); +const getAuthorizationFilter = jest.fn().mockResolvedValue({}); + +const clientArgs = { + logger, + services: { + caseService, + }, + authorization: { getAuthorizationFilter }, +} as unknown as CasesClientArgs; + +const constructorOptions = { casesClient: clientMock, clientArgs }; + +describe('Status', () => { + beforeAll(() => { + getAuthorizationFilter.mockResolvedValue({}); + clientMock.cases.get.mockResolvedValue({ id: '' } as unknown as Case); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns empty values when no features set up', async () => { + caseService.executeAggregations.mockResolvedValue(undefined); + const handler = new Status(constructorOptions); + expect(await handler.compute()).toEqual({}); + }); + + it('returns null when aggregation returns undefined', async () => { + caseService.executeAggregations.mockResolvedValue(undefined); + const handler = new Status(constructorOptions); + handler.setupFeature(CaseMetricsFeature.STATUS); + + expect(await handler.compute()).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('returns null when aggregation returns empty object', async () => { + caseService.executeAggregations.mockResolvedValue({}); + const handler = new Status(constructorOptions); + handler.setupFeature(CaseMetricsFeature.STATUS); + + expect(await handler.compute()).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('returns null when aggregation returns empty status object', async () => { + caseService.executeAggregations.mockResolvedValue({ status: {} }); + const handler = new Status(constructorOptions); + handler.setupFeature(CaseMetricsFeature.STATUS); + + expect(await handler.compute()).toEqual({ status: { open: 0, inProgress: 0, closed: 0 } }); + }); + + it('returns values when there is a status value', async () => { + caseService.executeAggregations.mockResolvedValue({ + status: { + buckets: [ + { key: CasePersistedStatus.OPEN, doc_count: 2 }, + { key: CasePersistedStatus.IN_PROGRESS, doc_count: 1 }, + { key: CasePersistedStatus.CLOSED, doc_count: 3 }, + ], + }, + }); + const handler = new Status(constructorOptions); + handler.setupFeature(CaseMetricsFeature.STATUS); + + expect(await handler.compute()).toEqual({ status: { open: 2, inProgress: 1, closed: 3 } }); + }); + + it('passes the query options correctly', async () => { + caseService.executeAggregations.mockResolvedValue({ + status: { + buckets: [ + { key: CasePersistedStatus.OPEN, doc_count: 2 }, + { key: CasePersistedStatus.IN_PROGRESS, doc_count: 1 }, + { key: CasePersistedStatus.CLOSED, doc_count: 3 }, + ], + }, + }); + const handler = new Status({ + ...constructorOptions, + from: '2022-04-28T15:18:00.000Z', + to: '2022-04-28T15:22:00.000Z', + owner: 'cases', + }); + + handler.setupFeature(CaseMetricsFeature.STATUS); + await handler.compute(); + + expect(caseService.executeAggregations.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "aggregationBuilders": Array [ + StatusCounts {}, + ], + "options": Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "isQuoted": false, + "type": "literal", + "value": "cases.attributes.created_at", + }, + "gte", + Object { + "isQuoted": false, + "type": "literal", + "value": "2022-04-28T15:18:00.000Z", + }, + ], + "function": "range", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "isQuoted": false, + "type": "literal", + "value": "cases.attributes.created_at", + }, + "lte", + Object { + "isQuoted": false, + "type": "literal", + "value": "2022-04-28T15:22:00.000Z", + }, + ], + "function": "range", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "isQuoted": false, + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "isQuoted": false, + "type": "literal", + "value": "cases", + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases/status.ts b/x-pack/plugins/cases/server/client/metrics/all_cases/status.ts new file mode 100644 index 0000000000000..0a9b9ce403d36 --- /dev/null +++ b/x-pack/plugins/cases/server/client/metrics/all_cases/status.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 type { CasesMetricsResponse } from '../../../../common/types/api'; +import { Operations } from '../../../authorization'; +import { createCaseError } from '../../../common/error'; +import { constructQueryOptions } from '../../utils'; +import { AllCasesAggregationHandler } from '../all_cases_aggregation_handler'; +import type { AggregationBuilder, AllCasesBaseHandlerCommonOptions } from '../types'; +import { StatusCounts } from './aggregations/status_counts'; + +export class Status extends AllCasesAggregationHandler { + constructor(options: AllCasesBaseHandlerCommonOptions) { + super( + options, + new Map<string, AggregationBuilder<CasesMetricsResponse>>([['status', new StatusCounts()]]) + ); + } + + public async compute(): Promise<CasesMetricsResponse> { + const { + authorization, + services: { caseService }, + logger, + } = this.options.clientArgs; + + try { + const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( + Operations.getCasesMetrics + ); + + const caseQueryOptions = constructQueryOptions({ + from: this.from, + to: this.to, + owner: this.owner, + authorizationFilter, + }); + + const aggregationsResponse = await caseService.executeAggregations({ + aggregationBuilders: this.aggregationBuilders, + options: { filter: caseQueryOptions.filter }, + }); + + return this.formatResponse<CasesMetricsResponse>(aggregationsResponse); + } catch (error) { + throw createCaseError({ + message: `Failed to calculate cases status counts: ${error}`, + error, + logger, + }); + } + } +} diff --git a/x-pack/plugins/cases/server/client/metrics/utils.test.ts b/x-pack/plugins/cases/server/client/metrics/utils.test.ts index 1cdf7e59e95e6..3248fd9b1e16d 100644 --- a/x-pack/plugins/cases/server/client/metrics/utils.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/utils.test.ts @@ -81,7 +81,7 @@ describe('utils', () => { ], [ { caseId: null }, - 'invalid features: [not-exists], please only provide valid features: [mttr]', + 'invalid features: [not-exists], please only provide valid features: [mttr, status]', ], ])('throws if the feature is not supported: %s', async (opts, msg) => { expect(() => diff --git a/x-pack/plugins/cases/server/client/metrics/utils.ts b/x-pack/plugins/cases/server/client/metrics/utils.ts index d20f64a2ff093..b2d9918a00de4 100644 --- a/x-pack/plugins/cases/server/client/metrics/utils.ts +++ b/x-pack/plugins/cases/server/client/metrics/utils.ts @@ -16,6 +16,7 @@ import { Connectors } from './connectors'; import { Lifespan } from './lifespan'; import type { GetCaseMetricsParams, MetricsHandler } from './types'; import { MTTR } from './all_cases/mttr'; +import { Status } from './all_cases/status'; const isSingleCaseMetrics = ( params: GetCaseMetricsParams | CasesMetricsRequest @@ -33,7 +34,7 @@ export const buildHandlers = ( (ClassName) => new ClassName({ caseId: params.caseId, casesClient, clientArgs }) ); } else { - handlers = [MTTR].map( + handlers = [MTTR, Status].map( (ClassName) => new ClassName({ owner: params.owner, diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index ea3866cbe1256..ff4f51fc0b47a 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -23,8 +23,6 @@ import { } from '../../common/constants'; import eksLogo from '../assets/icons/cis_eks_logo.svg'; -import aksLogo from '../assets/icons/cis_aks_logo.svg'; -import gkeLogo from '../assets/icons/cis_gke_logo.svg'; import googleCloudLogo from '../assets/icons/google_cloud_logo.svg'; export const CSP_MOMENT_FORMAT = 'MMMM D, YYYY @ HH:mm:ss.SSS'; @@ -145,34 +143,6 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { }), testId: 'cisEksTestId', }, - { - type: CLOUDBEAT_AKS, - name: i18n.translate('xpack.csp.kspmIntegration.aksOption.nameTitle', { - defaultMessage: 'AKS', - }), - benchmark: i18n.translate('xpack.csp.kspmIntegration.aksOption.benchmarkTitle', { - defaultMessage: 'CIS AKS', - }), - disabled: true, - icon: aksLogo, - tooltip: i18n.translate('xpack.csp.kspmIntegration.aksOption.tooltipContent', { - defaultMessage: 'Azure Kubernetes Service - Coming soon', - }), - }, - { - type: CLOUDBEAT_GKE, - name: i18n.translate('xpack.csp.kspmIntegration.gkeOption.nameTitle', { - defaultMessage: 'GKE', - }), - benchmark: i18n.translate('xpack.csp.kspmIntegration.gkeOption.benchmarkTitle', { - defaultMessage: 'CIS GKE', - }), - disabled: true, - icon: gkeLogo, - tooltip: i18n.translate('xpack.csp.kspmIntegration.gkeOption.tooltipContent', { - defaultMessage: 'Google Kubernetes Engine - Coming soon', - }), - }, ], }, vuln_mgmt: { diff --git a/x-pack/plugins/data_usage/common/constants.ts b/x-pack/plugins/data_usage/common/constants.ts new file mode 100644 index 0000000000000..bf8b801cbf92a --- /dev/null +++ b/x-pack/plugins/data_usage/common/constants.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 PLUGIN_ID = 'data_usage'; + +export const DEFAULT_SELECTED_OPTIONS = 50 as const; + +export const DATA_USAGE_API_ROUTE_PREFIX = '/api/data_usage/'; +export const DATA_USAGE_METRICS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}metrics`; +export const DATA_USAGE_DATA_STREAMS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}data_streams`; diff --git a/x-pack/plugins/data_usage/common/experimental_features.ts b/x-pack/plugins/data_usage/common/experimental_features.ts new file mode 100644 index 0000000000000..bf01916bfff3e --- /dev/null +++ b/x-pack/plugins/data_usage/common/experimental_features.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ServerlessExperimentalFeatures = Record< + keyof typeof allowedExperimentalValues, + boolean +>; + +/** + * A list of allowed values that can be used in `xpack.dataUsage.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze({ + /** + * <Add a description of the feature here> + * + * [This is a fake feature key to showcase how to add a new serverless-specific experimental flag. + * It also prevents `allowedExperimentalValues` from being empty. It should be removed once a real feature is added.] + */ + dataUsageDisabled: false, +}); + +type ServerlessExperimentalConfigKeys = Array<keyof ServerlessExperimentalFeatures>; +type Mutable<T> = { -readonly [P in keyof T]: T[P] }; + +const allowedKeys = Object.keys( + allowedExperimentalValues +) as Readonly<ServerlessExperimentalConfigKeys>; + +export type ExperimentalFeatures = ServerlessExperimentalFeatures; +/** + * Parses the string value used in `xpack.dataUsage.enableExperimental` kibana configuration, + * which should be a string of values delimited by a comma (`,`) + * The generic experimental features are merged with the serverless values to ensure they are available + * + * @param configValue + * @throws DataUsagenvalidExperimentalValue + */ +export const parseExperimentalConfigValue = ( + configValue: string[] +): { features: ExperimentalFeatures; invalid: string[] } => { + const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {}; + const invalidKeys: string[] = []; + + for (const value of configValue) { + if (!allowedKeys.includes(value as keyof ServerlessExperimentalFeatures)) { + invalidKeys.push(value); + } else { + enabledFeatures[value as keyof ServerlessExperimentalFeatures] = true; + } + } + + return { + features: { + ...allowedExperimentalValues, + ...enabledFeatures, + }, + invalid: invalidKeys, + }; +}; + +export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/plugins/data_usage/common/index.ts b/x-pack/plugins/data_usage/common/index.ts index 8b952b13d4cc7..63e34f872108c 100644 --- a/x-pack/plugins/data_usage/common/index.ts +++ b/x-pack/plugins/data_usage/common/index.ts @@ -5,15 +5,10 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - -export const PLUGIN_ID = 'data_usage'; -export const PLUGIN_NAME = i18n.translate('xpack.dataUsage.name', { - defaultMessage: 'Data Usage', -}); - -export const DEFAULT_SELECTED_OPTIONS = 50 as const; - -export const DATA_USAGE_API_ROUTE_PREFIX = '/api/data_usage/'; -export const DATA_USAGE_METRICS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}metrics`; -export const DATA_USAGE_DATA_STREAMS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}data_streams`; +export { + PLUGIN_ID, + DEFAULT_SELECTED_OPTIONS, + DATA_USAGE_API_ROUTE_PREFIX, + DATA_USAGE_METRICS_API_ROUTE, + DATA_USAGE_DATA_STREAMS_API_ROUTE, +} from './constants'; diff --git a/x-pack/plugins/data_usage/common/test_utils/index.ts b/x-pack/plugins/data_usage/common/test_utils/index.ts new file mode 100644 index 0000000000000..c3c8e75b29454 --- /dev/null +++ b/x-pack/plugins/data_usage/common/test_utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { TestProvider } from './test_provider'; +export { dataUsageTestQueryClientOptions } from './test_query_client_options'; +export { timeXMinutesAgo } from './time_ago'; diff --git a/x-pack/plugins/data_usage/common/test_utils/test_provider.tsx b/x-pack/plugins/data_usage/common/test_utils/test_provider.tsx new file mode 100644 index 0000000000000..a3d154ba911e0 --- /dev/null +++ b/x-pack/plugins/data_usage/common/test_utils/test_provider.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; + +export const TestProvider = memo(({ children }: { children?: React.ReactNode }) => { + return <I18nProvider>{children}</I18nProvider>; +}); diff --git a/x-pack/plugins/data_usage/common/test_utils/time_ago.ts b/x-pack/plugins/data_usage/common/test_utils/time_ago.ts new file mode 100644 index 0000000000000..7fe74e232bdac --- /dev/null +++ b/x-pack/plugins/data_usage/common/test_utils/time_ago.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const timeXMinutesAgo = (x: number) => + new Date(new Date().getTime() - x * 60 * 1000).toISOString(); diff --git a/x-pack/plugins/data_usage/common/utils.ts b/x-pack/plugins/data_usage/common/utils.ts new file mode 100644 index 0000000000000..ddd707b1134fd --- /dev/null +++ b/x-pack/plugins/data_usage/common/utils.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dateMath from '@kbn/datemath'; +export const dateParser = (date: string) => dateMath.parse(date)?.toISOString(); +export const momentDateParser = (date: string) => dateMath.parse(date); diff --git a/x-pack/plugins/data_usage/public/app/components/assets/illustration_product_no_results_magnifying_glass.svg b/x-pack/plugins/data_usage/public/app/components/assets/illustration_product_no_results_magnifying_glass.svg new file mode 100644 index 0000000000000..4329041f84a9f --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/assets/illustration_product_no_results_magnifying_glass.svg @@ -0,0 +1,32 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 200 148"> + <g fill="#e6ebf2"> + <path d="M66.493 121.253a.664.664 0 0 0 .373-.78.945.945 0 0 0-1.1-.386.686.686 0 0 0 .208 1.253c.178.041.364.01.519-.087zM46.666 68.12c.12-.113.373-.44.287-.6-.087-.16-.514-.046-.667 0-.153.047-.22.267-.073.454.146.186.3.293.453.146zm-1.26 13.006a.427.427 0 0 0-.58-.153c-.247.107-.46.253-.46.573 0 .947.173 1.16 1.1 1.334h.067a1.205 1.205 0 0 0-.367 0 .286.286 0 0 0-.2.426.34.34 0 0 0 .487.22.907.907 0 0 0 .273-.36.4.4 0 0 0 .54.147.474.474 0 0 0 .073-.767c-.226-.22-.146-.426-.12-.666.707.046 1-.267.88-.867a1.716 1.716 0 0 0-.053-.173c-.147-.394-.38-.487-.753-.3-.374.186-.6.326-.487.873-.2-.133-.333-.186-.4-.286zM45.42 80a.386.386 0 0 0 .166-.52.486.486 0 0 0-.507-.346.552.552 0 0 0-.46.666.58.58 0 0 0 .8.2zm59.546 54.133a.7.7 0 0 0 .2.914.71.71 0 0 0 .887-1.107 1.035 1.035 0 0 0-1.087.193zM47.08 69.04a.449.449 0 0 0-.22.666.5.5 0 0 0 .626.18.54.54 0 0 0 .167-.666.427.427 0 0 0-.574-.18zM46.987 64c.12-.086.346-.4.28-.507-.067-.106-.427-.126-.58-.1-.154.027-.247.26-.14.487a.266.266 0 0 0 .44.12zm4.486 44a.772.772 0 0 0-.107 1.033.892.892 0 0 0 1.2-.087.6.6 0 0 0 .203-.453.602.602 0 0 0-.203-.453.73.73 0 0 0-1.093-.04zm1.54-7.846a.177.177 0 0 0 .063-.048.17.17 0 0 0 .03-.15.168.168 0 0 0-.04-.069.16.16 0 0 0-.266-.053.166.166 0 0 0-.047.273.16.16 0 0 0 .119.087.16.16 0 0 0 .141-.04zm-3.28-8.374c.072.19.16.373.267.546-.05.084-.094.171-.133.26a.92.92 0 0 0 .173 1 1.02 1.02 0 0 0 .9.174 1.22 1.22 0 0 0 0 .5l.313.146a58.42 58.42 0 0 1-1.106-3.42.775.775 0 0 0-.18.094.607.607 0 0 0-.234.7zm2.807 10.347a.234.234 0 0 0 .28-.114.319.319 0 0 0-.113-.34c-.094-.06-.24.06-.287.154-.047.093.007.266.12.3zm.193 4.286a.664.664 0 0 0 .106-.713.354.354 0 0 0-.102-.104.34.34 0 0 0-.282-.043.35.35 0 0 0-.129.067c-.313.213-.453.447-.36.613a.776.776 0 0 0 .767.18zm.32-2.286a.795.795 0 0 0-.073-.947.801.801 0 0 0-.84.1.533.533 0 0 0 0 .667c.353.32.713.393.913.18zM44.5 73.374a.86.86 0 0 0-.394 1.08.873.873 0 0 0 1.033.5 1.12 1.12 0 0 0 .467-1.247.872.872 0 0 0-1.107-.333zm9.886 28.086c-.173-.326-.34-.666-.507-.986-.253.16-.353.393-.253.593a1.04 1.04 0 0 0 .76.393zm.794 4.713c-.12.26-.22.527.087.667.306.14.473-.034.6-.274a.51.51 0 0 0-.464-.427.512.512 0 0 0-.223.034zm-4.14-10.84c-.12.047-.167.12-.114.247.053.127.127.16.253.113a.174.174 0 0 0 .114-.24c-.047-.12-.12-.173-.254-.12zm4.147 12.867a.357.357 0 0 0-.052.42.357.357 0 0 0 .098.113.442.442 0 0 0 .516.11.435.435 0 0 0 .151-.11.4.4 0 0 0-.04-.587.463.463 0 0 0-.528-.065.462.462 0 0 0-.145.119zm.499 2.707a.482.482 0 0 0 .667 0 .442.442 0 0 0 0-.6.397.397 0 0 0-.444-.144.382.382 0 0 0-.143.084.434.434 0 0 0-.19.312.44.44 0 0 0 .11.348zm-1.353-6.241a.345.345 0 0 0-.058.282.34.34 0 0 0 .178.225c.313.213.586.253.666.107a.77.77 0 0 0-.12-.774.67.67 0 0 0-.666.16zM37.22 85.493c-.16-.3-.353-.293-.513-.207s-.3.26-.16.434c.14.173.34.22.446.166.107-.053.174-.286.227-.393zm1.087 7.047a.666.666 0 0 0-.347.747.74.74 0 0 0 .813.38c.26-.107.334-.527.16-.907a.52.52 0 0 0-.626-.22zm-.387-3.367c-.274-.106-.514-.273-.747 0a.4.4 0 0 0 0 .554.534.534 0 0 0 .406.18c.334-.087.38-.367.34-.734zM45 100.606c-.26 0-.48.56-.3.794.18.233.6.293.74.106.14-.186-.174-.84-.44-.9zm-1.786-5.046-.207.22a4.215 4.215 0 0 0-.407-.373.434.434 0 0 0-.666 0 .667.667 0 0 0-.174.78.6.6 0 0 0 .667.366c.201-.018.4-.059.593-.12.067.072.14.136.22.194a.913.913 0 0 0 1.013.073 1.061 1.061 0 0 0 .387-.96.534.534 0 0 0-.2-.347.833.833 0 0 0-1.227.167zm2.959 9.774a.352.352 0 0 0-.118.266.358.358 0 0 0 .118.267.32.32 0 0 0 .104.083.323.323 0 0 0 .261.018.311.311 0 0 0 .115-.068.35.35 0 0 0 .1-.513.398.398 0 0 0-.58-.053zm-5.84-19.148c0-.353-.213-.493-.567-.5-.193-.826-.666-.926-1.333-.313a.838.838 0 0 1-.167-.06.4.4 0 0 0-.533.113.433.433 0 0 0-.087.5c.154.467.36.567.82.38l1.04-.4c.287.62.367.64.827.28zm1 2.614a.787.787 0 1 0-1.334.747.874.874 0 0 0 1.187.147.614.614 0 0 0 .147-.894zm.614 3.2a.8.8 0 0 0-.94-.333.727.727 0 0 0-.26.9c.146.307.426.367.82.173.266-.133.473-.533.38-.74zm.893 5.94a.426.426 0 0 0-.467.273.387.387 0 0 0 .533.42.4.4 0 0 0-.067-.693zm1.5-31.093c.1-.047.093-.3.053-.367s-.187-.167-.287-.113a.26.26 0 0 0-.107.3c.047.073.247.233.34.18zm-1.294 8.893c.107.107.394.354.54.26.147-.093.04-.486 0-.6-.04-.113-.286-.086-.406-.04a.22.22 0 0 0-.134.38zm-.292-3.78a.76.76 0 0 0 .173-.98.713.713 0 0 0-1.18.78.726.726 0 0 0 1.007.2zM41.22 84.873c-.227.114-.307.3-.147.48s.313.46.56.327c.247-.133.113-.487.087-.667-.027-.18-.294-.246-.5-.14zm-.72-3.066a.926.926 0 0 0-.247 1.086.808.808 0 0 0 1.167.18.667.667 0 0 0 .293-.96.907.907 0 0 0-1.213-.306zm4.586-9.714a.76.76 0 0 0 .34-.88.52.52 0 0 0-.667-.246.667.667 0 0 0-.26.84.448.448 0 0 0 .587.286zm.5-2.367a.14.14 0 0 0 .033-.18c0-.04-.14-.046-.166 0a.123.123 0 0 0 .04.224.123.123 0 0 0 .093-.017zm-1.88 7.7a.667.667 0 0 0 .86.134c.387-.22.5-.467.34-.767a.774.774 0 0 0-.926-.333.847.847 0 0 0-.273.966zM40.8 74.787c.12-.113.386-.447.293-.587-.094-.14-.494-.086-.667-.046-.173.04-.213.346-.093.52.12.173.333.26.466.113zm-2.447-1.86c0 .04.18.133.22.106s.247-.14.2-.286c-.046-.147-.266-.12-.3-.087a.4.4 0 0 0-.12.267zm.247 3.553a.436.436 0 0 0 .8-.347.573.573 0 0 0-.627-.233.453.453 0 0 0-.173.58zm2.327-10.587c.073-.04.073-.207.053-.3s-.173-.193-.287-.133-.053.273 0 .36a.24.24 0 0 0 .234.073zM38 71.1a.466.466 0 0 0 .513-.333.346.346 0 0 0-.14-.467c-.293-.193-.52-.046-.767.16.06.3.047.607.394.64zm2.76 8.3a.7.7 0 0 0 .146-.827.667.667 0 0 0-.82.127.5.5 0 0 0 .667.666zm-2.387 1.266c.287-.16.367-.38.234-.666a.78.78 0 0 0-.954-.407.667.667 0 0 0-.126.873.627.627 0 0 0 .846.2zm7.233 27.268c-.133 0-.287.266-.413.433a.184.184 0 0 0-.047.061.184.184 0 0 0 0 .151.185.185 0 0 0 .047.061.56.56 0 0 0 .666-.1.404.404 0 0 0-.253-.606zm33.627 20.193c-.126-.254-.466-.32-.846-.154-.28.134-.487.507-.394.72a.847.847 0 0 0 .914.42.548.548 0 0 0 .12-.066.763.763 0 0 0 0 .386c.1.329.31.614.593.807.1.077.177.181.22.3a.584.584 0 0 0 .887.433.28.28 0 0 0 .033.167.523.523 0 0 0 .667.113.531.531 0 0 0 .313-.666.46.46 0 0 0-.277-.219.468.468 0 0 0-.35.045.86.86 0 0 0-.173-.42.832.832 0 0 1-.227-.586c0-.36-.253-.58-.5-.78a.827.827 0 0 0-.88.04l-.066.06a.786.786 0 0 0-.034-.6zm9.973 5.979c.1.2.414.14.554.114a.305.305 0 0 0 .225-.134.3.3 0 0 0 .035-.26c-.06-.213-.34-.4-.5-.28s-.414.36-.314.56zm4.247-1.332a.605.605 0 0 0-.287.866c.173.354.513.52.773.374a.893.893 0 0 0 .367-1.054.667.667 0 0 0-.853-.186zm.16 2.966c-.12.193.093.427.193.533a.292.292 0 0 0 .364.085.292.292 0 0 0 .103-.085c.146-.166.16-.5 0-.573s-.54-.147-.66.04zm-3.42-4.167a.447.447 0 0 0-.477-.299.459.459 0 0 0-.19.066.472.472 0 0 0 0 .833.269.269 0 0 0 .18.28c.142.02.285.02.427 0a.465.465 0 0 0 .033-.426.49.49 0 0 0 .027-.454zM76 127.926c.34-.167.486-.467.353-.72a.91.91 0 0 0-1.053-.373.668.668 0 0 0-.154.82.575.575 0 0 0 .853.273zm-9.22-3.706a.757.757 0 0 0 .32.14c.28-.146.373-.406.233-.546a.544.544 0 0 0-.406-.147c-.194.033-.26.4-.147.553zm4.733-.807c.3-.106.553-.446.48-.666-.147-.46-.667-.454-.92-.44-.254.013-.38.613-.14.96a.439.439 0 0 0 .58.146zm42.58 9.307c-.3.113-.22.366-.167.606.26.12.494.14.667-.126a.384.384 0 0 0-.053-.36.504.504 0 0 0-.447-.12zm-.533 1.873c.113.073.206.167.353.147s.2-.234.087-.4a.723.723 0 0 0-.474-.28.197.197 0 0 0-.096.012.211.211 0 0 0-.137.234.459.459 0 0 0 .267.287zm2.513-4.133c.353.047.493-.187.626-.487-.093-.113-.173-.233-.26-.32l-.666.147a.62.62 0 0 0-.054.3.355.355 0 0 0 .354.36zm5.14-1.587c.033 0 .14 0 .16-.053.02-.054 0-.127 0-.16a.14.14 0 0 0-.194 0c-.023.033-.033.073-.027.113s.028.076.061.1zm-6.633 3.247a.815.815 0 0 0 .754-.786.843.843 0 0 0-.898-.657.84.84 0 0 0-.762.81c-.034.347.44.6.906.633zm2.98 2.966a.256.256 0 0 0 0 .354c.133.126.28-.06.347-.127.066-.067 0-.227-.034-.24a.342.342 0 0 0-.313.013zm.473-4.626c0-.147 0-.433-.114-.473a.475.475 0 0 0-.407.146c-.06.087-.053.44.054.507.106.067.433-.113.467-.18zm-10.32 3.86a.421.421 0 0 0-.031.346.44.44 0 0 0 .237.254c.194.093.407.2.534 0 .206-.334.186-.62 0-.76a.483.483 0 0 0-.408-.093.498.498 0 0 0-.332.253zm-.38-2.113a.663.663 0 0 0-.007.433.66.66 0 0 0 .26.347.396.396 0 0 0 .318.035.393.393 0 0 0 .236-.215c.233-.354.253-.627.073-.767a.759.759 0 0 0-.88.167zm-3.44 4.06a.38.38 0 0 0 .14.56.517.517 0 0 0 .667-.134.502.502 0 0 0-.167-.633.452.452 0 0 0-.364-.036.464.464 0 0 0-.276.243zM112 131.2a.724.724 0 0 0 .967-.187.945.945 0 0 0 .046-.666l-1.4.213a.888.888 0 0 0 .387.64zm-1.747 1.993a.424.424 0 0 0 .506-.035.403.403 0 0 0 .107-.145c.267-.413.254-.7-.046-.893a.75.75 0 0 0-.854.213.857.857 0 0 0 .287.86zM50.88 97.454a.18.18 0 0 1 0 .113.899.899 0 0 0-.16.08c-.074-.1-.227-.12-.487.04a.466.466 0 0 0-.233.666.667.667 0 0 0 .666.367c.05.056.115.097.187.12.247.074.487.254.667.154a.447.447 0 0 0 .246-.314.666.666 0 0 0 .087-.426 1.094 1.094 0 0 0-.173-.38 1.134 1.134 0 0 0 0-.214c-.034-.266-.094-.666-.46-.613-.367.053-.367.287-.34.407zm-4.054-24.248c-.3 0-.613 0-.613.434a.48.48 0 0 0 .346.5.348.348 0 0 0 .287 0c.147-.167.267-.36.413-.56.235-.005.464-.07.667-.187v-.887a.92.92 0 0 0-.6-.26c-.4-.02-.5.18-.5.96zm1.08 11.214a.56.56 0 0 0 .667.174l.04-.047a35.31 35.31 0 0 1-.154-.84.667.667 0 0 0-.426 0 .489.489 0 0 0-.127.713zm-.866-7.933a.394.394 0 0 0 .306-.2c.094-.28-.066-.454-.346-.56-.2.126-.447.246-.334.546a.507.507 0 0 0 .374.214zm2.206 17.407c.087.153.207.3.427.213a.493.493 0 0 0 .186-.667 1.467 1.467 0 0 0-.486 0c-.2.074-.227.274-.127.454zm-.179-7.061a.587.587 0 0 0 .147.667c-.047-.24-.107-.454-.147-.667zm-.401 3.38a.833.833 0 0 0 1.047-.227.667.667 0 0 0 .053-.313c-.04-.14-.087-.273-.12-.413a.72.72 0 0 0-.4-.354.847.847 0 0 0-.993.267c-.22.267.026.747.413 1.04zm-1.14-12.593c-.406.093-.873.247-.973.74a1.14 1.14 0 0 0 1.14 1.267.386.386 0 0 1 .353.166 56.95 56.95 0 0 1-.166-3.213 6.074 6.074 0 0 0-.354 1.04zm3.407 24.806a.15.15 0 0 0 .055-.043.156.156 0 0 0 .034-.132.159.159 0 0 0-.029-.065c-.06-.073-.187-.213-.333-.153-.147.06-.06.287 0 .32s.153.147.273.073zm-.267-7.093a.42.42 0 0 0-.38-.38c-.233 0-.3.14-.28.62a.407.407 0 0 0 .66-.24zm-1.066 5.82a.242.242 0 0 0 .046.093.443.443 0 0 0 .36.174c.127 0 .254.066.434 0a.955.955 0 0 0 .52-.667.719.719 0 0 0-.287-.667.614.614 0 0 0-.62-.033.725.725 0 0 0-.427.533.28.28 0 0 0-.193 0c-.307.074-.667.067-.907.38l.04.454c-.313.28-.466.666-.326.86a.42.42 0 0 0 .5.16c.366-.12.446-.26.413-.72.18-.24.313-.387.447-.567zm-.76 3.514c.246-.24.22-.527-.08-.82a.454.454 0 0 0-.312-.195.456.456 0 0 0-.355.095.971.971 0 0 0-.12.987c.273.28.567.253.867-.067zm13.9 14.6c0-.134-.247-.174-.32-.154a.308.308 0 0 0-.2.24c0 .107.18.174.266.174s.267-.134.254-.26zM48.287 95.7a.16.16 0 0 0-.043.191.16.16 0 0 0 .043.055c.06.067.233.22.333.134s0-.28 0-.314c0-.033-.213-.16-.333-.066z"/> + <path d="M48.147 96.866c.113 0 .28.167.16.207s-.187.187-.094.493a.476.476 0 0 0 .587.387.7.7 0 0 0 .52-.833c-.08-.38-.306-.4-.333-.667s-.18-.313-.447-.353-.667-.074-.707.3c-.04.373.2.466.314.466zm.173-10.7c.266-.153.273-.553 0-.966a.606.606 0 0 0-.707-.14.872.872 0 0 0-.273.806.847.847 0 0 0 .98.3zm-1.327 2.187a1.333 1.333 0 0 0-1.333-.42.854.854 0 0 0-.267 1.12.993.993 0 0 0 1.167.373.833.833 0 0 0 .433-1.073zM42.96 78.92a1.494 1.494 0 0 0 0-.527c-.073-.247-.433-.293-.667-.14a.413.413 0 0 0-.093.587.553.553 0 0 0 .76.08zm2.62 17.88c-.06.227.066.327.533.42a.4.4 0 0 0-.067-.666.42.42 0 0 0-.467.246zm-3.513-16.387a.5.5 0 0 0 .666.14.48.48 0 0 0 .134-.626.507.507 0 0 0-.667-.134.46.46 0 0 0-.133.62zm3.966 12.541a.408.408 0 0 0-.52.3.533.533 0 0 0 .113.466c.26.207.52.074.747-.146-.007-.24-.04-.54-.34-.62zm-.7-1.887c.24-.207.12-.467 0-.747-.293 0-.6-.1-.726.247a.259.259 0 0 0 0 .1 1.067 1.067 0 0 0-.827.046.444.444 0 0 0-.113.627.667.667 0 0 0 .846.333.74.74 0 0 0 .36-.513.427.427 0 0 0 .46-.093zm2.141 2.306c.666.72 1.253.347 1.653-.16a.966.966 0 0 0 .213-.593 1 1 0 0 0-.38-.74c-.247-.227-.253-.567-.567-.667a1.373 1.373 0 0 0-1.593.467 1.106 1.106 0 0 0-.213 1.04c.127.36.62.347.887.653zm-3.947-10.6c.28-.16.32-.466.106-.846-.213-.38-.46-.434-.666-.294a1.172 1.172 0 0 0-.36.947.667.667 0 0 0 .92.193zm1.926 4.22a.666.666 0 0 0 .206-.666.46.46 0 0 0-.666-.2.512.512 0 0 0-.227.62.726.726 0 0 0 .687.246zm-1.62 5.674a.807.807 0 0 0-.38.98.82.82 0 0 0 1.067.273.567.567 0 0 0 .193-.78c-.153-.32-.633-.573-.88-.473zm-.693-6.634c.113.213.78.287 1.047.113a.667.667 0 0 0 .16-.706.667.667 0 0 0-.847-.327.76.76 0 0 0-.36.92zm1.026 3.014a.806.806 0 0 0 .373-1.22.9.9 0 0 0-1.213-.3 1.073 1.073 0 0 0-.207 1.273.732.732 0 0 0 1.047.247zm49.127 45.106c.033-.234-.38-.667-.667-.707-.287-.04-.527-.08-.667.253-.14.334-.226.474-.346.714a.392.392 0 0 0-.156.31.386.386 0 0 0 .156.31.497.497 0 0 0 .666-.047c.12-.118.26-.213.414-.28a.67.67 0 0 0 .6-.553zm5.013.147v.033c-.067.267.18.353.373.433.267-.113.414-.293.294-.56-.04-.093-.227-.206-.307-.18l-.153.08a.368.368 0 0 0 .04-.133.404.404 0 0 0-.374-.321.408.408 0 0 0-.173.028.387.387 0 0 0-.187.306.472.472 0 0 0 .22.36.31.31 0 0 0 .267-.046zm-2.026 2.88a.449.449 0 0 0 .042-.529.444.444 0 0 0-.129-.137.552.552 0 0 0-.666.073.456.456 0 0 0 .113.667.432.432 0 0 0 .337.109.427.427 0 0 0 .303-.183zm1.393-5.314a.406.406 0 0 0-.468-.029.407.407 0 0 0-.119.116.441.441 0 0 0 .04.667.468.468 0 0 0 .35.098.476.476 0 0 0 .317-.178.508.508 0 0 0-.12-.674zm-.713 6.801a.674.674 0 0 0-.94.126.672.672 0 0 0 .127.94.91.91 0 0 0 1.1-.24c.16-.206.006-.64-.287-.826zm-1.287-3.22a.452.452 0 0 0 .344.045.446.446 0 0 0 .27-.218.46.46 0 0 0 .086-.362.46.46 0 0 0-.214-.305.55.55 0 0 0-.666.213.493.493 0 0 0 .18.627zm-1.806 4.46c-.06.173-.14.606 0 .713s.493-.187.5-.253c0-.2.08-.46-.1-.607s-.367.06-.4.147z"/> + <path d="M95.72 132.54a.7.7 0 0 0 .48-.286.919.919 0 0 0 .153-.74c1.378.16 2.76.278 4.146.353-.033.113-.053.227-.08.347-.284.05-.549.177-.766.366l-.187-.1a.846.846 0 0 0-.947.5.853.853 0 0 0 .747.914.886.886 0 0 0 .667-.207l.093.08c.38.3.713.14 1.033-.127.194.18.32.474.667.287a.433.433 0 0 0 .22-.513c-.1-.367-.407-.3-.667-.267-.06-.16-.113-.307-.173-.453l.58-.254c.031-.162.047-.327.047-.493l1.62.04h.066a.67.67 0 0 0 .2 0h1.467a.667.667 0 0 0 .32.627.823.823 0 0 0 1.2-.287.663.663 0 0 0 .093-.387l1.147-.053a.505.505 0 0 0 .507-.507.507.507 0 0 0-.507-.506c-12.3.12-24.54-2.667-34.913-9.414a57.165 57.165 0 0 1-15.814-15.646 30.06 30.06 0 0 1-2.213-3.62.197.197 0 0 0-.074-.153c-.023-.019-.05-.032-.079-.039s-.058-.007-.087-.002l-.18-.313a.816.816 0 0 0-.6.087.59.59 0 0 0-.136.659.59.59 0 0 0 .136.194.441.441 0 0 0 .667.107l.06.14a.383.383 0 0 0 .086.587c.06.032.127.046.194.039a30.134 30.134 0 0 0 1.58 2.967.491.491 0 0 0 .126.413.406.406 0 0 0 .214.12c.133.22.273.447.413.667-.167.267-.133.507.153.787a.806.806 0 0 0 .54.233c.12.167.234.347.36.513a.872.872 0 0 0-.533.207c-.413.327-.353 1.013 0 1.127.353.113.533-.074.6.053.067.127.08.447.333.5s.46-.28.614-.513c.333.444.673.889 1.02 1.333a.6.6 0 0 0 .073.5.717.717 0 0 0 .527.227c.067.073.127.153.193.233.42.5.86 1 1.333 1.487a.579.579 0 0 0-.34.153.667.667 0 0 0 .12.827.58.58 0 0 0 .9 0 .592.592 0 0 0 .094-.134c.8.84 1.633 1.647 2.486 2.434a.477.477 0 0 0 0 .413.66.66 0 0 0 .487.247.327.327 0 0 0 .14-.06c.514.453 1.04.893 1.567 1.333a1.101 1.101 0 0 0 0 .947.763.763 0 0 0 .98.44.958.958 0 0 0 .4-.314c.36.28.733.554 1.1.82.055.07.13.123.213.154a41.756 41.756 0 0 0 3.74 2.446.5.5 0 0 0 0 .28.797.797 0 0 0 .967.407.21.21 0 0 0 .08-.073c.493.28.986.553 1.486.813a.513.513 0 0 0 .047.267c.213.473.753.78 1.113.626a.71.71 0 0 0 .234-.173 34.4 34.4 0 0 0 2.073.96.491.491 0 0 0 .567.247c.726.313 1.46.613 2.2.893 0 .253.273.3.52.26v-.053l.98.353a.62.62 0 0 0 0 .213.445.445 0 0 0 .587.254l.08-.04a.853.853 0 0 0 .727.52h.06a.785.785 0 0 0-.06.873c.052.12.124.23.213.327a.377.377 0 0 0-.267.186.602.602 0 0 0 .313.8.763.763 0 0 0 .814-.16.604.604 0 0 0 0-.726.86.86 0 0 0 .38-1.014.95.95 0 0 0-.394-.44c1.273.374 2.56.667 3.853.974a.277.277 0 0 0 0 .227.283.283 0 0 0 .167.153c.142.02.285.02.427 0a.598.598 0 0 0 .066-.247c.56.116 1.12.222 1.68.32a.435.435 0 0 0 .094.1.394.394 0 0 0 .546 0c.314.053.627.107.94.147a1.32 1.32 0 0 0-.16.34.369.369 0 0 0-.293-.134.586.586 0 0 0-.4.225.59.59 0 0 0-.12.442.754.754 0 0 0 .553.613.613.613 0 0 0 .634-.366.951.951 0 0 0 .433.086.145.145 0 0 0 .2.054.144.144 0 0 0 .054-.054zm7.167 5.613a.448.448 0 0 0-.387.127c-.073.093-.153.407-.04.507s.413-.047.513-.127a.319.319 0 0 0-.086-.507zm-2.22 4.093a.494.494 0 0 0 .153.667.46.46 0 0 0 .541-.002.488.488 0 0 0-.014-.778.467.467 0 0 0-.68.113zm1.08-7.066a.61.61 0 0 0-.88.22.718.718 0 0 0 .073.927.891.891 0 0 0 .98-.214.604.604 0 0 0 .134-.507.624.624 0 0 0-.307-.426zm-3.467 5.134a.153.153 0 0 0 .053.18c.034 0 .14 0 .16-.054.02-.053 0-.126 0-.16a.14.14 0 0 0-.213.034zm-51.867-38.808c.054-.073.16-.226.06-.353s-.293 0-.313.053a.255.255 0 0 0 0 .307.173.173 0 0 0 .058.042.169.169 0 0 0 .195-.049zm54.254 36.094a.669.669 0 0 0-.114-.22.582.582 0 0 0-.57-.252.58.58 0 0 0-.47.412 3.65 3.65 0 0 0-.206.94.907.907 0 0 0 .666.813c.207.04.667-.033.667-.46-.347-.62.153-.707.027-1.233zm-2.487 5.426a.5.5 0 0 0-.187.38c.067.294.44.314.58.24a.395.395 0 0 0 .127-.314.396.396 0 0 0-.16-.299.368.368 0 0 0-.36-.007zm1.313-7.313c-.113-.16-.667-.126-.72.12a.424.424 0 0 0 .273.473c.24.047.567-.433.447-.593zM84 135.147c-.187.106-.147.26-.08.406.066.147.313.3.473.16.095-.108.173-.229.233-.36a.452.452 0 0 0-.626-.206zm1.333 3.726a.24.24 0 0 0-.1.24c.04.08.207.1.3.087.094-.013.2-.153.154-.273s-.254-.08-.354-.054zm2.347-5.319a.9.9 0 0 0-.933.186c-.167.2-.347.407-.127.667s.286.44.433.667a.377.377 0 0 0 .338.355.498.498 0 0 0 .502-.635 1.158 1.158 0 0 1 0-.5.668.668 0 0 0-.213-.74zm4.24 4.692c-.054.047-.114.287 0 .36.113.074.28-.066.346-.126.067-.06.054-.194 0-.24a.333.333 0 0 0-.346.006z"/> + <path d="M85.466 131.287a.194.194 0 0 1-.271-.217.196.196 0 0 1 .078-.117.865.865 0 0 0-.287-1.166.955.955 0 0 0-.773-.06v-.04a.523.523 0 0 0-.544-.306.535.535 0 0 0-.21.072.582.582 0 0 0-.378.357.572.572 0 0 0 .065.517.772.772 0 0 0 .54.446c.03.169.123.319.26.42.209.096.443.122.667.074a.546.546 0 0 0 0 .353.43.43 0 0 0 .547.24.405.405 0 0 0 .306-.573zM103.767 134a.61.61 0 0 0-.054-.813.498.498 0 0 0-.666.053.713.713 0 0 0 .173.873.398.398 0 0 0 .547-.113zm-21.101 2.334c-.08 0-.26.2-.22.3s.294.12.367.086a.266.266 0 0 0 .14-.28.262.262 0 0 0-.287-.106zm1.427-3.001a.453.453 0 0 0-.446-.259.43.43 0 0 0-.175.053c-.186.106-.146.26-.08.406a.3.3 0 0 0 .474.16 1.25 1.25 0 0 0 .226-.36zm6.013 1.934a1.085 1.085 0 0 0-1.333.567c-.113.36.333.566.593.493s.187 0 .387.187a.477.477 0 0 0 .66.097.47.47 0 0 0 .153-.191.933.933 0 0 0-.46-1.153zm.507 1.326a.31.31 0 0 0-.14.26c0 .067.18.247.293.2s.087-.267.067-.36a.185.185 0 0 0-.092-.09.183.183 0 0 0-.128-.01zm1.207-4.019a.449.449 0 0 0-.413-.291.453.453 0 0 0-.174.031.541.541 0 0 0-.28.6.443.443 0 0 0 .421.274.446.446 0 0 0 .172-.041.426.426 0 0 0 .273-.573zm-3.874-1.28c-.067.093.047.313.113.353a.2.2 0 0 0 .073.037.203.203 0 0 0 .159-.018.209.209 0 0 0 .106-.206.207.207 0 0 0-.078-.14c-.06-.053-.32-.113-.373-.026zm-.973 5.559a1.1 1.1 0 0 0-.214 1.453c.247.28.667 0 .727-.253s.087-.167.36-.233a.473.473 0 0 0 .392-.268.467.467 0 0 0-.045-.472.947.947 0 0 0-1.22-.227zm26.62 4.327a.139.139 0 0 0-.009.098.145.145 0 0 0 .055.082c.04 0 .147 0 .16-.054.014-.053.034-.126 0-.16a.14.14 0 0 0-.052-.029.13.13 0 0 0-.114.018.123.123 0 0 0-.04.045zm3.773-1.747a.459.459 0 0 0 .114.667.356.356 0 0 0 .456.049.357.357 0 0 0 .11-.122.437.437 0 0 0 .041-.503.467.467 0 0 0-.428-.218.474.474 0 0 0-.293.127zm-2.1-5.807a.757.757 0 0 0-.048.872.743.743 0 0 0 .214.222.947.947 0 0 0 1.24-.2.994.994 0 0 0-.22-1.2.816.816 0 0 0-.661-.11.829.829 0 0 0-.525.416zm-.453 7.487c-.053.04-.08.307 0 .353.08.047.28-.06.347-.126.067-.067 0-.227 0-.24a.35.35 0 0 0-.347.013zm.98-4.193c-.093.133 0 .4.247.54a.247.247 0 0 0 .4-.08c.107-.18.153-.373-.047-.533a.477.477 0 0 0-.6.073zm-1.88-1.353a.386.386 0 0 0 .06.533c.133.14.62 0 .667-.153a.529.529 0 0 0-.147-.44.353.353 0 0 0-.305-.118c-.057.005-.111.025-.159.056s-.088.072-.116.122zm-.24 3.513a.302.302 0 0 0 .093.306c.087.047.267.134.32 0a.301.301 0 0 0-.04-.313c-.046-.02-.32-.093-.373.007zm9.874-8.354c-.067.094.046.314.113.354a.214.214 0 0 0 .154.044.211.211 0 0 0 .177-.15.22.22 0 0 0-.018-.159.227.227 0 0 0-.053-.062c-.06-.053-.307-.087-.373-.027zm-10.121 9.161a.735.735 0 0 0-.76.113.468.468 0 0 0 .18.62.442.442 0 0 0 .51-.037.428.428 0 0 0 .11-.143c.147-.22.14-.44-.04-.553zm-9.06-2.414c-.266-.186-.573-.086-.813.267a.5.5 0 0 0 .04.707.96.96 0 0 0 1-.1.584.584 0 0 0-.025-.717.584.584 0 0 0-.202-.157zm22.927-7.433a.226.226 0 0 0-.26.04c-.047.067 0 .22.087.293.086.074.233.094.313 0 .08-.093-.1-.28-.14-.333zm-4.867 4.4c-.066.134-.14.514 0 .58.14.067.447-.073.567-.166.12-.094.107-.34-.087-.5a.26.26 0 0 0-.268-.113.27.27 0 0 0-.212.199zm-2.393-5.153a.51.51 0 0 0-.666.12.533.533 0 0 0 .16.666.419.419 0 0 0 .58-.12.467.467 0 0 0-.074-.666zm1.3 1.246c-.18-.087-.38-.113-.46.087s-.133.56 0 .666c.134.107.474-.18.58-.306.107-.127.114-.314-.12-.447zm-1.093 8.607c-.054.107-.094.34 0 .413a.416.416 0 0 0 .413-.04c.073-.066.147-.413.047-.486-.1-.074-.42.04-.46.113zm-1.234-8.313a.8.8 0 0 0-.926.366c-.194.307-.08.607.306.84a.624.624 0 0 0 .754-.153 1.076 1.076 0 0 0-.134-1.053zm-11.6 5.733c-.4-.287-.84-.307-1.02-.047a.86.86 0 0 0 .234.967.782.782 0 0 0 .86-.213.57.57 0 0 0-.074-.707zm.947 3.326a.454.454 0 0 0 .098-.353.448.448 0 0 0-.191-.313.504.504 0 0 0-.667.113.53.53 0 0 0 .153.667c.047.033.1.057.157.07a.435.435 0 0 0 .45-.184zm-.507 4.248c-.093.093-.173.626 0 .766.174.14.627-.213.74-.373a.515.515 0 0 0-.106-.593c-.167-.134-.48.066-.634.2zm-2.9-3.754a.451.451 0 0 0-.348-.111.452.452 0 0 0-.319.177.352.352 0 0 0-.116.297.357.357 0 0 0 .17.27.43.43 0 0 0 .525.087.43.43 0 0 0 .141-.127.44.44 0 0 0-.053-.593zm3.647.76c-.18-.1-.373-.167-.533 0a.554.554 0 0 0 .187.666c.146 0 .3-.106.413-.226a.285.285 0 0 0 .069-.109.277.277 0 0 0-.136-.331zm-3.113-2.234a.57.57 0 0 0 .24.727.61.61 0 0 0 .9-.147.621.621 0 0 0-.054-.82.901.901 0 0 0-1.086.24zm-.62 4a.505.505 0 0 0 .087.667.49.49 0 0 0 .666-.14.532.532 0 0 0-.146-.667.413.413 0 0 0-.495.005.396.396 0 0 0-.112.135zm5.98-5.36a.544.544 0 0 0-.667.18.665.665 0 0 0 .107.82.81.81 0 0 0 .946-.12c.107-.193-.086-.673-.386-.88zm.533 5.454a.313.313 0 0 0 .1.347c.047.02.098.027.149.019a.277.277 0 0 0 .137-.059.292.292 0 0 0 0-.287c-.086-.053-.32-.14-.386-.02zm1.293-10.333a.67.67 0 0 0-.053-.873c-.227-.26-.507-.24-.787.18a7.979 7.979 0 0 0-.187.96.41.41 0 0 0 .387.46.429.429 0 0 0 .487-.354c.034-.13.085-.256.153-.373zm.361 4.206c.113-.093.053-.527-.147-.667s-.293 0-.393.147a.3.3 0 0 0 .113.487c.147.073.314.126.427.033zm-2.788-2.433c-.046-.533-.153-.72-.446-.747a.443.443 0 0 0-.48.274.515.515 0 0 0 .133.666c.313.16.553-.033.793-.193zm-.68-2.853a.716.716 0 0 0 .833.047.988.988 0 0 0 .074-.927c-.667.073-1.334.147-2 .2.046.167.16.313.313.3a.778.778 0 0 1 .78.38zm.741 4.013a.45.45 0 0 0-.393-.147.697.697 0 0 0-.367.407c-.067.353.26.38.527.5.246-.24.426-.474.233-.76zm-17.32.033c-.214.073-.14.347-.08.42s.446.433.606.373.1-.526.04-.566-.347-.3-.567-.227zM56.666 111.58c-.24.1-.153.667.18.947a.458.458 0 0 0 .607 0c.246-.2.38-.614.233-.807-.28-.387-.76-.247-1.02-.14zm-.086 4.04a.749.749 0 0 0-.134.98.806.806 0 0 0 1 .047c.2-.167.153-.667-.073-.933a.547.547 0 0 0-.598-.197.558.558 0 0 0-.196.103zm-.734-1.133c-.166-.12-.526 0-.666.1a.36.36 0 0 0 .046.54c.031.034.07.06.114.076a.282.282 0 0 0 .263-.034.28.28 0 0 0 .09-.102c.113-.12.307-.454.153-.58zm2.627 5.113c-.12-.167-.38 0-.454.053s-.173.24-.053.367a.29.29 0 0 0 .367 0c.08-.02.26-.247.14-.42zm-2.567 2.4a.27.27 0 0 0 0 .274.304.304 0 0 0 .3 0c.053-.054.166-.234 0-.36-.167-.127-.207.046-.3.086zm-.219-4.747a.321.321 0 0 0 0 .174c.04.015.086.015.126 0 0-.054.087-.127.047-.18-.04-.054-.113.006-.173.006zm4.866-.433a.569.569 0 0 0-.093.447c.053.12.466.26.613.12a.42.42 0 0 0 0-.593.32.32 0 0 0-.52.026zm1.34-.946a.404.404 0 0 0 .071-.536.403.403 0 0 0-.144-.131c-.127-.047-.36.16-.534.273a.184.184 0 0 0-.1.116.182.182 0 0 0 .02.151.551.551 0 0 0 .687.127zm-3.133 4.792c-.2.047-.447.507-.3.667.146.16.433.36.666 0 .234-.36-.166-.74-.366-.667zm2.873 2.754c-.087.093-.267.38-.067.573.2.194.427-.033.494-.113a.439.439 0 0 0 0-.407.286.286 0 0 0-.201-.126.291.291 0 0 0-.121.011.286.286 0 0 0-.105.062zm.167-1.807c.073 0 .18-.173.133-.28-.047-.107-.207-.133-.287-.107-.08.027-.253.2-.213.3s.293.12.366.087zm.74-1.113a.104.104 0 0 0-.053.054c-.207 0-.667.366-.534.6a.452.452 0 0 0 .554.22c.147-.074.187-.34.153-.547h.074c.053 0 .206-.16.16-.28-.047-.12-.267-.073-.354-.047zm-7.873-3.233c-.2.146-.054.513-.08.726l.126.074a.432.432 0 0 0 .474-.2.6.6 0 0 0 .16-.367c-.22-.067-.494-.373-.68-.233zm5.473.733c-.167.06-.16.44 0 .56.12.054.25.079.38.073.253-.22.3-.486.133-.573a.655.655 0 0 0-.513-.06zm-.047-2.667c-.16-.667-.786-.367-.733-.8s.427-.2.46-.82-.48-1.013-.86-.8-.353.607-.453.627-.567 0-.58.353c0 .733.666.587.666.807s-.1.666-.04.98c.087.466.42.84.9.666a.8.8 0 0 0 .546-.364.807.807 0 0 0 .094-.649zm-11.227-8a.339.339 0 0 0-.092.409.339.339 0 0 0 .092.118.622.622 0 0 0 .8.167.724.724 0 0 0 .107-.82c-.193-.2-.613-.134-.907.126zm6.52 5.827a.744.744 0 0 0-.06-.886.588.588 0 0 0-.667-.04c-.333.306-.433.733-.22.946a.81.81 0 0 0 .947-.02zm-7.646-7.513a.667.667 0 0 0 0 .773.453.453 0 0 0 .667 0 .518.518 0 0 0 .066-.666.662.662 0 0 0-.733-.107zm3.513-2.387a1.04 1.04 0 0 0-.593-.186c-.247-.087-.8.046-.887.26-.086.213.147.373.374.46a.867.867 0 0 0 .213.353c.26.253.573.213.887-.113a.669.669 0 0 0 .006-.774zm-3.22 9.44a.222.222 0 0 0 0 .254c.06.066.22 0 .307 0s.14-.214.06-.314-.293.027-.367.06zm-.393-13.113c-.154-.28-.234-.634-.594-.78l-.426.146a1.213 1.213 0 0 0-.354-.167.115.115 0 0 0 0-.046c-.053-.127-.266-.094-.306-.06l-.087.073a.32.32 0 0 0-.16.093.42.42 0 0 0 0 .527c.207.327.36.367.793.227.253.143.513.273.78.387a.3.3 0 0 0 .354-.4zm33.287 33.246a.63.63 0 0 0-.343.358.622.622 0 0 0 .03.495c.1.274.446.36.826.207a.437.437 0 0 0 .298-.23.45.45 0 0 0 .015-.377.967.967 0 0 0-.826-.453zM49.96 105.247c-.134-.147-.28-.12-.434.04 0 .06-.033.166 0 .213a.97.97 0 0 1 .167.607c0 .1.22.246.347.26a.575.575 0 0 0 .407-.194.456.456 0 0 0-.04-.38 4.377 4.377 0 0 0-.447-.546zm3.113 7.966c-.126-.166-.32-.073-.46.06-.14.134-.166.434 0 .514.167.08.427.253.574.04.146-.214-.014-.494-.114-.614zm.574 1.033a.563.563 0 0 0-.427 0c-.167.094-.14.467.047.567a.81.81 0 0 0 .346.033c.214-.233.214-.513.034-.6zm.679-4.579a1.026 1.026 0 0 0-1.273-.067c-.353.28-.32.833.067 1.287a.757.757 0 0 0 .513.272.755.755 0 0 0 .553-.179.994.994 0 0 0 .14-1.313zm-3.313 7.82a.415.415 0 0 0-.164.333.42.42 0 0 0 .164.333.387.387 0 0 0 .281.149.4.4 0 0 0 .3-.109c.206-.206.28-.62.132-.766a.662.662 0 0 0-.713.06zM51.16 114a.559.559 0 0 0-.067.713c.153.167.54.127.854-.153 0-.087 0-.274-.04-.387a.555.555 0 0 0-.747-.173zm-.213-2.754a.698.698 0 0 0-.148.504.686.686 0 0 0 1.214.356.624.624 0 0 0 .094-.853.92.92 0 0 0-1.16-.007zm-3.514 2.427c-.066.047-.18.28-.107.36.074.08.32 0 .374-.04a.206.206 0 0 0 .065-.222.206.206 0 0 0-.102-.122.195.195 0 0 0-.078-.023.2.2 0 0 0-.152.047zM74 131.293a.304.304 0 0 0-.267.16c-.04.1.107.227.194.253.086.027.293-.04.326-.166.034-.127-.166-.247-.253-.247zm2.113 1.127c-.066-.194-.513-.16-.753.1s.067.62.233.666c.167.047.587-.58.52-.766zm.04-2.8c-.1-.18-.246-.293-.44-.227a2.668 2.668 0 0 0-1.286.814.836.836 0 0 0-.087.226.573.573 0 0 0 .793.667c.308-.105.599-.25.867-.433a.916.916 0 0 0 .153-1.047zm-3.253 3.447a.468.468 0 0 0 .446.393c.273-.04.433-.667.273-.773-.16-.107-.746.113-.72.38zm-1.053 2.033c-.107.06-.374.273-.247.52s.413.107.507.053a.431.431 0 0 0 .16-.373.285.285 0 0 0-.29-.235.28.28 0 0 0-.13.035zm1.486-5.88a.43.43 0 0 0 .193-.56.326.326 0 0 0-.5-.154.57.57 0 0 0-.226.394c-.02.133.32.433.533.32zm-1.106.266c-.18 0-.3.367-.174.54.127.174.26.154.334.194.313-.127.446-.36.32-.5a.732.732 0 0 0-.48-.234zm3.513 8.887a.363.363 0 0 0 .066.34.41.41 0 0 0 .42 0c.087-.106-.073-.386-.153-.426a.358.358 0 0 0-.333.086zm3.766-3.746a.429.429 0 0 0 .32-.507.568.568 0 0 0-.267-.34c-.24-.113-.413 0-.666.447.146.213.246.513.613.4zm-7.359-6.907a.817.817 0 0 0 .337-.023.808.808 0 0 0 .583-.757c.06-.667-.627-.607-.434-1s.474-.047.707-.62c.233-.573-.12-1.113-.554-1.04-.433.073-.526.453-.626.447-.1-.007-.54-.187-.667.14-.266.666.407.76.394.986-.014.227-.314.587-.367.92-.074.467.126.927.626.947zm7.493 10.66a.968.968 0 0 0-.434 1.126.88.88 0 0 0 1.087.507 1.334 1.334 0 0 0 .493-1.273.879.879 0 0 0-1.147-.36zm-.307-5.906a.49.49 0 0 0 .21-.609.486.486 0 0 0-.577-.285.48.48 0 0 0-.273.587.525.525 0 0 0 .64.307zm-1.253-1.681a.461.461 0 0 0-.42.135.463.463 0 0 0-.12.425.622.622 0 0 0 .38.387.529.529 0 0 0 .453-.18.54.54 0 0 0 .033-.453.53.53 0 0 0-.327-.314zm-2.04 4.187a.678.678 0 0 0-.394.353.665.665 0 0 0-.013.527 1.06 1.06 0 0 0 1.2.56.773.773 0 0 0 .347-.986.835.835 0 0 0-.808-.533.83.83 0 0 0-.332.079zM63.773 126a.433.433 0 0 0-.236.255.421.421 0 0 0 .036.345.384.384 0 0 0 .216.234.393.393 0 0 0 .318-.007c.266-.12.473-.493.38-.667a.704.704 0 0 0-.714-.16zm3.127 5.9a.297.297 0 0 0-.067.267.316.316 0 0 0 .287.107c.066 0 .233-.167.14-.334-.094-.166-.28-.08-.36-.04zm.38-4.907c-.234.073-.22.46-.314.667.033.036.064.074.093.113a.431.431 0 0 0 .514 0 .578.578 0 0 0 .266-.3c-.16-.14-.32-.553-.56-.48zm-.333-1.627c.046-.126-.194-.333-.287-.346a.341.341 0 0 0-.28.206.366.366 0 0 0 .18.3.407.407 0 0 0 .386-.16zm-2.174-5.886c-.126-.153-.54 0-.666.347s.266.567.446.573c.18.007.347-.76.22-.92zm1.827 3.654c.12.113.28.246.44.133s.14-.467.086-.62a.195.195 0 0 0-.153-.14.777.777 0 0 0-.2-.527.848.848 0 0 0-.94-.238.847.847 0 0 0-.287.185.667.667 0 0 0-.14.887.424.424 0 0 0-.347-.047.559.559 0 0 0-.3.667c.094.2.467.293.86.133 0-.087.12-.253.087-.38a.46.46 0 0 0-.067-.14 1.153 1.153 0 0 0 .96.087zm-2.52-5.254a.912.912 0 0 0-.187-1.053.36.36 0 0 0-.493-.067c-.43.293-.76.709-.947 1.194a.743.743 0 0 0 0 .24.582.582 0 0 0 .69.536.574.574 0 0 0 .29-.15c.244-.205.461-.44.647-.7zm5.566 8.187a.766.766 0 0 0-.447.886.797.797 0 0 0 .927.374c.24-.094.367-.58.24-.907a.556.556 0 0 0-.5-.385.567.567 0 0 0-.22.032zm.407 5.459c-.207 0-.587.334-.514.56.074.227.294.487.667.254.373-.234.053-.787-.153-.814zm-1.86-4.052s.107.08.12.066c.014-.013.12-.093.1-.153-.02-.06-.093-.047-.146-.067a.445.445 0 0 0-.074.154zm1.654 3.346c.093 0 .333-.167.28-.367-.054-.2-.36-.14-.447-.1s-.24.167-.173.327a.282.282 0 0 0 .34.14zm1.419-6.907a.88.88 0 0 0-1.2.04.974.974 0 0 0-.04 1.207.884.884 0 0 0 .573.295.884.884 0 0 0 .62-.175 1.28 1.28 0 0 0 .047-1.367zm-1.933-.58a.739.739 0 0 0 .233-.86.575.575 0 0 0-.62-.26c-.413.18-.666.547-.513.827a.813.813 0 0 0 .9.293zm0 1.427c-.114-.173-.494-.167-.634-.113a.345.345 0 0 0-.192.38c.01.05.03.098.059.14.018.042.046.08.081.11a.287.287 0 0 0 .379-.01c.113-.087.413-.334.307-.507zm105.246-49.873h-8.253v8.253h8.253zm19.087 19.087h-8.253v8.253h8.253zm-19.087 0h-8.253v8.253h8.253zm-19.08-19.087h-8.253v8.253h8.253zm0 19.087h-8.253v8.253h8.253zm-21.106-19.087h-8.253v8.253h8.253z"/> + </g> + <path fill="#1ba9f5" d="M105.439 32.813a42.666 42.666 0 1 0 0 85.333 42.666 42.666 0 0 0 0-85.332zm0 69.394A26.751 26.751 0 0 1 79.2 70.232a26.753 26.753 0 1 1 50.956 15.465 26.744 26.744 0 0 1-24.717 16.51z"/> + <path fill="#0a89db" d="M133.935 93.105 123.47 103.57l27.143 27.144 10.465-10.465z"/> + <path fill="#0d90e0" d="M73.086 74.833a.48.48 0 0 0 .574-.393.58.58 0 0 0-.4-.6.754.754 0 0 0-.614.493c-.026.16.24.46.44.5zm10.287-15.347c.147.054.234-.18.227-.226-.007-.047-.033-.22-.173-.247a.153.153 0 0 0-.182.073.154.154 0 0 0-.018.067c-.007.094-.007.28.146.334zm1-.493c.154-.2.307-.407.474-.6a.433.433 0 0 0-.147-.107c-.28-.086-.667.087-.667.32-.033.12.147.274.34.387zM69.126 76.967a.465.465 0 0 0 .514-.427c.053-.28-.14-.666-.38-.666a.58.58 0 0 0-.667.433.549.549 0 0 0 .533.66zm.327-7.981.22-.226h-.707c.143.11.31.188.487.226zm-4.119 1.401a.426.426 0 0 0 .373-.053.9.9 0 0 0 .293-1 .433.433 0 0 0-.5-.327.74.74 0 0 0-.666.62c-.004.06-.004.12 0 .18v.04c.15.195.317.376.5.54zm1.852-1.627h-.407.054a.587.587 0 0 0 .353 0zm-3.633 8.44a1.114 1.114 0 0 0 .473.847c.28.08.613-.2.707-.6.093-.4-.06-.574-.5-.667a.559.559 0 0 0-.68.42zm5.294-3.427A.92.92 0 0 0 69 73.16c-.08-.207-.333-.34-.533-.533.106-.187.233-.42 0-.667-.62 0-.667.127-.387.667-.12.224-.2.468-.233.72.043.233.157.447.326.613.174.213.494.12.674-.187zm-4.474-1.347a1.273 1.273 0 0 0 1.294.44c.326-.146.253-.586.34-.666.086-.08.593.233.986 0a1.072 1.072 0 0 0 .38-1.187 1.04 1.04 0 0 0-.5-.6.866.866 0 0 0-.953.167c-.207.26-.213.766-.333.82-.12.053-.6-.287-.98-.054a.761.761 0 0 0-.234 1.08zm5.673 3.134a.592.592 0 0 0 .44-.213c.193-.34-.047-.553-.32-.753-.26.106-.553.206-.547.546a.406.406 0 0 0 .427.42zm7.2-19.266c.273.073.593-.22.667-.614a.347.347 0 0 0-.287-.44c-.32-.093-.707.073-.76.313a.728.728 0 0 0 .38.74zM75.24 58.62c-.08.16.1.547.306.667a.341.341 0 0 0 .473-.22c.14-.347.127-.62-.04-.707a.773.773 0 0 0-.74.26zm1.366 2.713h.52a.78.78 0 0 0 .36-.447.54.54 0 0 0-.386-.54c-.46-.073-.8.067-.847.354a.767.767 0 0 0 .353.633zm4.187-5.18a.407.407 0 0 0 .513-.28.367.367 0 0 0-.3-.44.334.334 0 0 0-.42.24.354.354 0 0 0 .207.48zm-.713 1.18c-.334-.073-.567.107-.667.52-.1.414.12.567.526.667a.473.473 0 0 0 .587-.273.974.974 0 0 0-.447-.914zm.22-3.953c.113-.074.086-.387.1-.594a.187.187 0 0 0-.167-.22.547.547 0 0 0-.52.467.406.406 0 0 0 .587.347zm-1.134 3.42a.666.666 0 0 0 .667-.34.666.666 0 0 0-.447-.666.46.46 0 0 0-.513.393.52.52 0 0 0 .293.613zm-1.673.36a.567.567 0 0 0-.226.38c0 .094.14.247.246.294.216.086.44.153.667.2.2.046.307-.054.34-.274-.033-.046-.06-.153-.12-.173a.974.974 0 0 1-.48-.413.466.466 0 0 0-.427-.014zm-4.16 3.02c-.047-.28-.114-.553-.454-.513s-.373.3-.34.567a.508.508 0 0 0 .794-.054zm3.613 14.947a.513.513 0 0 0 .58-.307.706.706 0 0 0-.326-.666.627.627 0 0 0-.614.373.467.467 0 0 0 .36.6zm.454 1.74a.767.767 0 0 0 .88-.447c.06-.233-.387-.733-.667-.787a.627.627 0 0 0-.18 1.234zm-2.8-7.713v.12c-.073.186-.187.26-.36.12-.06-.054-.1-.134-.167-.18a.806.806 0 0 0-.906 0 .666.666 0 0 0-.307.826 1.16 1.16 0 0 0 .78.827c-.055.18-.093.366-.113.553 0 .547.36.76.84.487a.613.613 0 0 0 .18-1.013 4.33 4.33 0 0 0-.42-.367c.186-.113.306-.253.426-.253a.58.58 0 0 0 .627-.594 1.09 1.09 0 0 1 .12-.273c.14-.307.107-.54-.093-.667h-.32a.606.606 0 0 0-.287.414zm-2.32 3.286c-.34 0-.714-.106-.987.26a.765.765 0 0 1-.594.347.726.726 0 0 0-.606.667 1.779 1.779 0 0 0 .986.986c.34-.153.794-.34 1.24-.546.054 0 .08-.187.067-.274a.86.86 0 0 1 .173-.666c.2-.294.027-.76-.28-.774zm2.233.42c-.293-.067-.573.227-.666.667a.593.593 0 0 0 .426.573.853.853 0 0 0 .74-.413.834.834 0 0 0-.5-.827zM78 71.593a.84.84 0 0 0-1.027.527 1.333 1.333 0 0 0 .667 1.186.853.853 0 0 0 .946-.666.987.987 0 0 0-.586-1.047zm-2.18 6.994c-.32-.067-.547.133-.667.56s.053.626.313.666a1.16 1.16 0 0 0 .9-.466.667.667 0 0 0-.546-.76zm2.886-3.64c0-.445.02-.89.06-1.334a.927.927 0 0 0-.327.54.84.84 0 0 0 .267.794zm.167 3.587c0-.214-.047-.434-.067-.667-.06.087-.106.187-.146.247-.04.06.006.34.213.42zM68.18 76a.667.667 0 0 0-.32-.44c-.374-.147-.488.207-.7.453.192.32.366.567.732.447a.374.374 0 0 0 .287-.46zm8.793-6.366a.813.813 0 0 0-.927-.534.847.847 0 0 0 .547 1.58c.326-.066.453-.586.38-1.046zm-11.94 6.626c-.06.22.093.58.293.593.314.071.643.026.927-.126.4-.287.426-.327.193-.84-.24 0-.433-.06-.627-.1a.727.727 0 0 0-.786.473zM79.16 59.893c.04-.3-.293-.667-.666-.727s-.594.147-.667.594a.667.667 0 0 0 .42.666c.266.034.88-.3.913-.533zM74.453 57.2a.74.74 0 0 0 .953-.594.786.786 0 0 0-.493-.92c-.354-.113-.827.26-.947.747a.606.606 0 0 0 .487.767zm.447 3.767a.347.347 0 0 0-.387-.353c-.373 0-.626.12-.666.306a.619.619 0 0 0 .246.407h.594a.666.666 0 0 0 .213-.36z"/> + <path fill="#0d90e0" d="M72.466 69.22a.62.62 0 0 0 .153-.46h-2.58c.094.373 0 .52-.42.393a.767.767 0 0 0-.933.373.78.78 0 0 0 0 .954.628.628 0 0 0 .9.213c.23-.135.425-.32.573-.54.127-.173.234-.3.454-.287.32 0 .426-.173.426-.466a.94.94 0 0 0 1.427-.18zm-1.92 2.614a.54.54 0 0 0 .6.293.467.467 0 0 0 .454-.407c.04-.34-.454-.873-.667-.84a.988.988 0 0 0-.387.954zm-.952-.048a.547.547 0 0 0-.614.354.706.706 0 0 0 .327.766.786.786 0 0 0 .9-.42c.06-.266-.207-.56-.613-.7zm2.619-13.272c0-.254-.12-.374-.34-.42a.447.447 0 0 0-.554.34.394.394 0 0 0 .36.46.473.473 0 0 0 .534-.38zm-4.46 18.399c-.32-.086-.54.227-.6.407s.287.533.667.433a.492.492 0 0 0 .287-.32.433.433 0 0 0-.354-.52zm-.42-2.539c-.446-1.147-1.12-1.247-1.88-.334a1.57 1.57 0 0 1-.233-.073.494.494 0 0 0-.667.2.613.613 0 0 0 0 .707c.314.666.614.766 1.187.466.433-.22.88-.426 1.333-.666.534.84.667.866 1.187.32a.74.74 0 0 0-.927-.62zm3.887-13.661a.38.38 0 0 0 .28.507.46.46 0 0 0 .58-.287.446.446 0 0 0-.36-.526.406.406 0 0 0-.5.306zm8.893.621h.76a.327.327 0 0 0-.186-.154.442.442 0 0 0-.394.067c-.053.027-.12.047-.18.087zm-12.587 24.16a.753.753 0 0 0-.847.426.52.52 0 0 0 .34.627.666.666 0 0 0 .747-.447.453.453 0 0 0-.24-.606zm3.734-2.514a.827.827 0 0 0 .846-.533c.067-.26-.213-.607-.546-.667-.434-.093-.667 0-.767.347a.78.78 0 0 0 .466.853zm.214 1.16c0-.147-.054-.527-.227-.554-.173-.026-.353.334-.38.467-.027.133.16.26.28.307s.347-.014.327-.22zm1.039 2.214c-.166 0-.586.073-.613.24-.027.166.313.393.46.473.147.08.393-.113.42-.327.027-.213-.053-.393-.267-.386zm-.98 4.52a.354.354 0 0 0-.207.44c.087.34.354.38.667.393.147-.266.36-.48.134-.74a.474.474 0 0 0-.594-.093zm-2.14-7.541a1.113 1.113 0 0 0-1.18.62.887.887 0 0 0 .6.987.86.86 0 0 0 .58-1.607zm1.219-5.186a.467.467 0 0 1-.133-.507.546.546 0 0 0-.513-.553c-.293 0-.373.153-.393.393.005.174-.03.348-.1.507-.2.32-.454.607-.667.927-.907-.254-1.18-.094-1.193.74a.62.62 0 0 0 .813.666 5.64 5.64 0 0 0 1-.546 1.1 1.1 0 0 0 1.22.1 1.147 1.147 0 0 0-.033-1.727zM67.56 82.82c-.225.033-.448.08-.667.14-.447-.347-.887-.407-1.093-.147a.92.92 0 0 0 .166 1.1c.3.247.507.173 1.02-.413.247.18.48.393.754.093a.48.48 0 0 0 .073-.607c-.067-.08-.18-.173-.253-.166zm1.606 4.327a.753.753 0 0 0-.78.62c-.073.386.16.666.607.74a.625.625 0 0 0 .793-.534.734.734 0 0 0-.62-.826zM73 89.14c-.087 0-.28-.054-.34.086s.113.26.16.26a.366.366 0 0 0 .267-.12s-.04-.213-.087-.226zm-3.86-7.806a.527.527 0 0 0-.14-.414.393.393 0 0 0-.354-.053c-.26.146-.253.38-.12.666.233.014.5.087.613-.2zm3.867-1.26a.587.587 0 0 0-.447-.667.386.386 0 0 0-.467.273.493.493 0 0 0 .147.6.547.547 0 0 0 .767-.206zm5.759 4.213a.666.666 0 0 0-.52-.667c-.32-.073-.533.04-.6.32a.787.787 0 0 0 .44.94.706.706 0 0 0 .68-.593zm-.433-3.62c.1-.414-.213-.794-.746-.914a.667.667 0 0 0-.86.52.9.9 0 0 0 .666 1.04.92.92 0 0 0 .94-.646zm-4.667 1.166c-.093.093-.293.207-.373.373-.08.167.126.514.406.554a.407.407 0 0 0 .46-.374.547.547 0 0 0-.493-.553zm-.613-27.573a.933.933 0 0 0 .967-.667.67.67 0 0 0-1.333-.113.626.626 0 0 0 .367.78zM76.82 88.7a.729.729 0 0 0-1.06 0 .667.667 0 0 0 .053.787.527.527 0 0 0 .707.16c.207-.1.667-.187.707-.527s-.287-.306-.407-.42zm.96-19.94a.48.48 0 0 0 .046.3 1.387 1.387 0 0 0 1.48.72c.075-.343.155-.683.24-1.02zm-2.994 12.906a.46.46 0 0 0 .547-.333.475.475 0 1 0-.934-.18.534.534 0 0 0 .387.513zM76 83.42a.44.44 0 0 0-.38-.473c-.367-.047-.667.373-.667.52a.667.667 0 0 0 .667.453.446.446 0 0 0 .38-.5zm-1.573-6.187-.04-.06c.083.093.183.17.293.227a.288.288 0 0 0 .433-.187.345.345 0 0 0-.22-.486.827.827 0 0 0-.44.086.413.413 0 0 0-.306-.473.473.473 0 0 0-.567.52c0 .32-.18.413-.373.593-.494-.506-.92-.473-1.227.06a.886.886 0 0 0-.08.16c-.153.394-.047.62.353.734.4.113.667.153.954-.327.055.157.091.32.106.487a.434.434 0 0 0 .327.506.667.667 0 0 0 .727-.12c.64-.713.653-.993.06-1.72zM74.98 86a.494.494 0 0 0-.528.306.566.566 0 0 0 .314.594.44.44 0 0 0 .513-.314.507.507 0 0 0-.3-.586zm3.84 1.06a.408.408 0 0 0-.52.273.46.46 0 0 0 .3.527.388.388 0 0 0 .52-.266.48.48 0 0 0-.3-.534zm-9.007-31.853a.56.56 0 0 0 .58-.34c.106-.44-.047-.847-.347-.907a.813.813 0 0 0-.78.547c-.06.24.26.653.547.7zm-3.2 37.873a.267.267 0 0 0-.073-.16zM66 60.3c.146 0 .413 0 .466-.173.053-.173-.127-.38-.253-.493s-.287 0-.387.12l-.067.18a.278.278 0 0 0 .122.327.28.28 0 0 0 .118.04zm1.166 1.033h.414c.093-.07.196-.127.306-.167a.587.587 0 0 0 .467-.62c0-.333-.227-.666-.18-.793.047-.127.153-.333.393-.24.24.093.627-.047.794-.547s-.274-1.04-.667-.926c-.393.113-.4.353-.533.293s-.314-.327-.554-.227-.253.874-.193.987c.06.113.347.347.153.533-.193.187-.526.234-.586.507-.06.273-.227.913.126 1.187zm1.007-4.313c.46.16.76-.254.906-.474.147-.22-.266-.667-.666-.667a.447.447 0 0 0-.487.36c-.107.287.013.7.247.78zm3.307-1.373a1 1 0 0 0 .62 1.167 1.033 1.033 0 0 0 1.1-.667c.127-.426-.2-.866-.78-1.026a.751.751 0 0 0-.94.526zm-1.2.9a.48.48 0 0 0-.554.346.446.446 0 0 0 .347.494.393.393 0 0 0 .52-.287.426.426 0 0 0-.313-.553zm.873-3.38c.207.073.313-.12.347-.307.033-.186-.1-.446-.294-.42-.193.027-.486 0-.493.287-.007.287.293.393.44.44zm-1.74 6.746a1 1 0 0 0 .727.973c.267.047.553-.226.613-.593.06-.367-.093-.667-.553-.753-.36-.067-.733.106-.787.373zm.687-7.247a.538.538 0 0 0 .36-.233c.086-.173-.147-.467-.354-.447a.74.74 0 0 0-.306.167c-.047.28.106.513.3.513zM66.413 80.38c.133-.048.262-.106.387-.174.413-.2.553-.46.44-.806a.72.72 0 0 0-.907-.467 7.66 7.66 0 0 1-1.333.26c-.367 0-.78.307-.754.56a.547.547 0 0 0 .534.367c.32-.107.413.12.606.233.194.09.407.13.62.12a.974.974 0 0 0 .407-.093zm-2.16 5.16a.433.433 0 0 0-.24 0l.22.874a.433.433 0 0 0 .366-.347.514.514 0 0 0-.346-.527zm.34-3.107a.993.993 0 0 0-.534-.727c-.253-.066-.44.094-.513.447-.073.353 0 .613.287.667.386.093.706-.074.76-.387zm2.467-1.98a.58.58 0 0 0-.667.3c-.134.447 0 .86.28.94a.773.773 0 0 0 .82-.48.667.667 0 0 0-.433-.76zm-1.487 6.467a.14.14 0 0 0-.146.114c0 .04.073.126.106.126a.134.134 0 0 0 .14-.08.132.132 0 0 0-.052-.138.134.134 0 0 0-.048-.022zm-2.106-11.1a.746.746 0 0 0-.214-.847.46.46 0 0 0-.446.054v1.293h.046a.8.8 0 0 0 .547-.107.294.294 0 0 0 .067-.393zm-.134-3.554a.726.726 0 0 0-.174-.973.771.771 0 0 0 .214-.227.532.532 0 0 0 0-.546c.068.006.138.006.206 0a1 1 0 0 0 .767-1.12.34.34 0 0 0 .2 0 .393.393 0 0 0 .127-.567h-.54a.447.447 0 0 0-.107.293.668.668 0 0 0-.727-.106 41.81 41.81 0 0 0-.4 3.806c.14-.1.294-.233.434-.56z"/> + <path fill="#294492" d="M33.566 123.433a1.635 1.635 0 0 1-1.433-.846 33.005 33.005 0 0 1-1.673-3.58 1.633 1.633 0 0 1 .92-2.114 1.633 1.633 0 0 1 1.77.385c.148.154.265.336.343.535a28.107 28.107 0 0 0 1.5 3.214 1.617 1.617 0 0 1-.619 2.196 1.61 1.61 0 0 1-.808.21zm-3.44-10.826a1.624 1.624 0 0 1-1.613-1.454 29.025 29.025 0 0 1-.18-3.233v-.733A1.633 1.633 0 0 1 30 105.6a1.632 1.632 0 0 1 1.136.509 1.62 1.62 0 0 1 .444 1.164v.667c0 .962.053 1.924.16 2.88a1.628 1.628 0 0 1-1.44 1.793zm1.127-11.274a1.622 1.622 0 0 1-1.623-1.503 1.64 1.64 0 0 1 .076-.63c.41-1.264.911-2.496 1.5-3.687a1.63 1.63 0 0 1 2.92 1.447 23.352 23.352 0 0 0-1.333 3.246 1.631 1.631 0 0 1-1.54 1.127zm5.8-9.733a1.627 1.627 0 0 1-1.173-2.753 28.727 28.727 0 0 1 2.933-2.667 1.626 1.626 0 1 1 2 2.547 25.898 25.898 0 0 0-2.6 2.373 1.605 1.605 0 0 1-1.16.48zm9.333-6.433a1.626 1.626 0 0 1-.666-3.12 34.99 34.99 0 0 1 3.686-1.334 1.628 1.628 0 1 1 .96 3.114 30.74 30.74 0 0 0-3.333 1.233 1.673 1.673 0 0 1-.627.086zm33.88-2.667a1.627 1.627 0 1 1-.033-3.254c1.233 0 2.48-.066 3.707-.12a1.629 1.629 0 0 1 .153 3.254c-1.26.053-2.533.1-3.793.12zm-7.68 0h-.046c-1.24-.031-2.5-.078-3.78-.14a1.628 1.628 0 1 1 .153-3.253c1.253.06 2.493.106 3.713.133a1.634 1.634 0 0 1-.04 3.26zm-15.173-.107a1.627 1.627 0 0 1-.173-3.247 48.211 48.211 0 0 1 3.866-.266 1.602 1.602 0 0 1 1.68 1.58 1.626 1.626 0 0 1-1.58 1.673c-1.226.04-2.446.12-3.613.247zm34.26-.553a1.627 1.627 0 0 1-.16-3.247c1.233-.127 2.473-.273 3.673-.433a1.628 1.628 0 0 1 .427 3.226c-1.233.167-2.5.314-3.773.447zm11.333-1.707a1.63 1.63 0 0 1-1.617-1.462 1.627 1.627 0 0 1 1.291-1.758 95.372 95.372 0 0 0 3.6-.813 1.63 1.63 0 0 1 .773 3.166c-1.213.294-2.467.574-3.72.834a1.816 1.816 0 0 1-.34-.027zM114 77.107a1.628 1.628 0 0 1-1.399-2.462c.198-.331.507-.581.872-.705a71.72 71.72 0 0 0 3.44-1.273 1.63 1.63 0 0 1 1.207 3.026c-1.167.46-2.38.907-3.594 1.333-.17.056-.348.083-.526.08zm10.473-4.547a1.634 1.634 0 0 1-.78-3.06 52.344 52.344 0 0 0 3.14-1.84 1.627 1.627 0 0 1 1.747 2.746 56.688 56.688 0 0 1-3.334 1.954 1.565 1.565 0 0 1-.746.2zm9.333-6.52a1.628 1.628 0 0 1-1.577-2.005 1.62 1.62 0 0 1 .491-.828c.9-.82 1.76-1.667 2.553-2.527a1.628 1.628 0 0 1 2.4 2.2c-.86.933-1.793 1.86-2.78 2.747a1.62 1.62 0 0 1-1.033.413zm7.167-8.834a1.628 1.628 0 0 1-1.427-2.413c.347-.667.667-1.28.967-1.92.193-.44.393-.88.593-1.333a1.638 1.638 0 0 1 1.536-.965 1.636 1.636 0 0 1 1.479 1.05 1.622 1.622 0 0 1-.035 1.248l-.606 1.333c-.334.727-.667 1.447-1.087 2.154a1.619 1.619 0 0 1-1.367.846zm-9.133-8.086a1.641 1.641 0 0 1-.607-.12 34.34 34.34 0 0 1-3.507-1.667 1.631 1.631 0 0 1-.895-1.61 1.632 1.632 0 0 1 1.182-1.413 1.627 1.627 0 0 1 1.26.163c1.048.57 2.127 1.08 3.233 1.527a1.628 1.628 0 0 1 .432 2.774 1.63 1.63 0 0 1-1.045.366zm13.586-2.453a1.621 1.621 0 0 1-1.619-1.509 1.628 1.628 0 0 1 .079-.632 53.13 53.13 0 0 0 1.027-3.493 1.629 1.629 0 0 1 2.977-.426c.22.371.283.815.176 1.233a52.294 52.294 0 0 1-1.093 3.713 1.63 1.63 0 0 1-1.547 1.114zm-23.086-3.694a1.612 1.612 0 0 1-1.167-.493 19.48 19.48 0 0 1-2.533-3.2 1.627 1.627 0 1 1 2.753-1.74 16.037 16.037 0 0 0 2.113 2.666 1.625 1.625 0 0 1-1.166 2.76zm25.333-7.487h-.04a1.63 1.63 0 0 1-1.478-1.04 1.63 1.63 0 0 1-.109-.626v-.627a19.053 19.053 0 0 0-.2-2.833 1.634 1.634 0 0 1 .772-1.636 1.638 1.638 0 0 1 1.233-.184 1.632 1.632 0 0 1 1.215 1.34c.165 1.103.245 2.218.24 3.333v.713a1.626 1.626 0 0 1-1.66 1.56zm-30-2.666A1.632 1.632 0 0 1 116 31.333c0-.42-.04-.853-.04-1.28v-.107c.006-.91.077-1.82.213-2.72a1.62 1.62 0 0 1 .651-1.064 1.63 1.63 0 0 1 2.569 1.571 15.184 15.184 0 0 0-.18 2.233V30c0 .36 0 .707.04 1.053a1.633 1.633 0 0 1-1.513 1.74zm26.893-8.093a1.62 1.62 0 0 1-1.286-.627 13.32 13.32 0 0 0-2.367-2.34 1.638 1.638 0 0 1-.29-2.29 1.63 1.63 0 0 1 2.29-.29 16.563 16.563 0 0 1 2.953 2.92 1.63 1.63 0 0 1 .18 1.712 1.631 1.631 0 0 1-1.46.915zm-23.533-2.5a1.628 1.628 0 0 1-1.2-2.727 13.671 13.671 0 0 1 3.333-2.667 1.629 1.629 0 0 1 1.614 2.827 10.443 10.443 0 0 0-2.527 2 1.605 1.605 0 0 1-1.24.567zm14-3.467c-.142 0-.283-.02-.42-.06a15.16 15.16 0 0 0-3.333-.513 1.627 1.627 0 0 1-1.56-1.694 1.653 1.653 0 0 1 1.693-1.56c1.37.057 2.73.267 4.053.627a1.627 1.627 0 0 1-.42 3.2z"/> + <path fill="#1ba9f5" d="M139.447 49.727c-.62.433-8.36 5.033-15.687 6.246a26.738 26.738 0 0 1-9.68 44.8c.42 5.374.98 10.74 1.58 16.1a42.667 42.667 0 0 0 23.787-67.146z"/> + <path fill="#0066b1" d="M133.333 107.814a.7.7 0 0 0 .047-.147c-.207.18-.4.36-.607.527a.605.605 0 0 0 .56-.38zm4.574-1.901a1.085 1.085 0 0 0 1-.353.667.667 0 0 0-.294-.667c0-.226-.18-.366-.52-.44a.574.574 0 0 0-.666.42.963.963 0 0 0 .166.52.667.667 0 0 0 .314.52zm1.28-1.393c.313.073.642.03.926-.12l.047-.04a.391.391 0 0 0 .38-.214.969.969 0 0 0-.307-.946.48.48 0 0 0-.39-.016.465.465 0 0 0-.167.109.465.465 0 0 0-.109.167.725.725 0 0 0-.667.473c-.073.213.087.573.287.587zm-.521-1.187c0-.053-.04-.167-.093-.187a.833.833 0 0 1-.353-.393c-.154.18-.3.367-.46.547.17.092.346.175.526.246.154.1.274.014.38-.213zm-1.293.38c-.067.08-.134.153-.207.227a.573.573 0 0 0 .107-.04.28.28 0 0 0 .07-.083.271.271 0 0 0 .03-.104zM137 107.7c.011.062.041.12.084.167a.334.334 0 0 0 .163.093c.106 0 .193-.154.186-.26-.006-.107-.113-.234-.226-.214a.221.221 0 0 0-.143.069.225.225 0 0 0-.064.145zm.653 4.34c.066-.167.106-.354-.1-.467a.482.482 0 0 0-.362.006.482.482 0 0 0-.258.254c.08.107.153.293.293.38s.353.02.427-.173zm-1.96-1.307c.16.105.35.154.54.14.111.032.225.05.34.053.06-.1.12-.193.167-.286.057-.039.11-.081.16-.127a2.78 2.78 0 0 0-.153-.56.666.666 0 0 0-.734-.387.738.738 0 0 0-.573.747.64.64 0 0 0 .083.236.636.636 0 0 0 .17.184zm5.093-10.786-.26-.254-.193.28a.868.868 0 0 0 .453-.026zm-10.653 10.28.106.113a.382.382 0 0 0 .074-.12.984.984 0 0 0 0-.133zm11.506-4.801a.497.497 0 0 0 .294-.326.421.421 0 0 0-.168-.445.428.428 0 0 0-.152-.069c-.32-.093-.54.22-.6.4s.28.534.626.44zm-11.285 4.994.32.32a.395.395 0 0 0-.32-.32zm8.099-.32a1.03 1.03 0 0 0-.54-.726.375.375 0 0 0-.194 0h-.04a.558.558 0 0 0-.273.426c-.087.394 0 .607.287.667.386.113.706-.053.76-.367zm-1.827 2.247c.147-.033.407-.16.393-.287a.47.47 0 0 0-.28-.32c-.106 0-.433.114-.453.24-.02.127.273.38.34.367zm7.034-1.014h-.08a.581.581 0 0 0-.327-.293 1.107 1.107 0 0 0-1.18.613.873.873 0 0 0 .6.987.858.858 0 0 0 1.013-.54.992.992 0 0 0 0-.447.138.138 0 0 0 .054-.053.169.169 0 0 0-.01-.225.169.169 0 0 0-.07-.042zm-.667-2.34a.477.477 0 0 0-.134-.407.386.386 0 0 0-.36-.06c-.253.147-.253.38-.113.667.233.02.493.093.607-.2zm-1.28 3.9a.416.416 0 0 0 .506.194c.207-.1.214-.254 0-.667a.393.393 0 0 0-.207-.007c-.068.015-.132.049-.183.097s-.089.109-.11.176a.404.404 0 0 0-.006.207zm2.953-8.007a.596.596 0 0 0-.5 0 .572.572 0 0 0-.313-.133c-.287 0-.367.153-.387.393a2.159 2.159 0 0 1-.06.374.164.164 0 0 0-.038-.082.17.17 0 0 0-.075-.052.169.169 0 0 0-.187.047.177.177 0 0 0-.033.06c0 .093-.053.28.087.353h.06c-.167.24-.36.48-.554.727-.9-.253-1.173-.093-1.186.747a.616.616 0 0 0 .222.558.617.617 0 0 0 .591.108c.161-.053.315-.124.46-.213.247.187.473.48.86.467l.327-.314c.42.067.806-.046.873-.28a.396.396 0 0 0-.067-.386 1.207 1.207 0 0 0-.24-1.427.568.568 0 0 1-.126-.18.48.48 0 0 0 .346.1c.22-.1.174-.733-.06-.867zm2.841 1.84h.08l-.4-.4a.4.4 0 0 0 .32.4zm-.661-.726-.94-.934-.066.094a1.234 1.234 0 0 0-.074.166c-.153.387-.046.614.354.727a.777.777 0 0 0 .726-.053zm.02 1.747a.589.589 0 0 0-.447-.7.374.374 0 0 0-.403.137.367.367 0 0 0-.063.136.48.48 0 0 0 .146.593c.061.041.13.069.202.082a.545.545 0 0 0 .565-.248zm-6.319 1.586a.766.766 0 0 0 .813-.48.667.667 0 0 0-.446-.76.554.554 0 0 0-.48.107c.048-.092.081-.191.1-.294l.12-.053c.42-.207.56-.467.446-.813a.72.72 0 0 0-.358-.419.72.72 0 0 0-.548-.048 8.425 8.425 0 0 1-1.334.267c-.36 0-.773.3-.746.553.026.253.42.407.533.367.287-.1.393.08.56.2a.482.482 0 0 0 0 .093.705.705 0 0 0 .553.507.593.593 0 0 0 .5-.147c-.126.467 0 .867.287.92zm1.173-6.08c-.374-.147-.494.213-.7.453.193.32.366.567.733.447a.361.361 0 0 0 .242-.169.38.38 0 0 0 .045-.291.72.72 0 0 0-.32-.44zm-.027-2.233c.043.233.157.447.327.613a.35.35 0 0 0 .48 0l-.794-.793a.421.421 0 0 0-.013.18zm1.42 2.547a.57.57 0 0 0-.626.433.558.558 0 0 0 .275.6.554.554 0 0 0 .225.067.482.482 0 0 0 .513-.434c.053-.306-.167-.66-.387-.666zm.786-.34a.547.547 0 0 0 .14 0l-.533-.534a.47.47 0 0 0 0 .14.405.405 0 0 0 .393.394z"/> + <path fill="#0066b1" d="M134.973 107.933c-.16.367-.086.667.18.78a.913.913 0 0 0 .514-.053.977.977 0 0 0 .4.6c.4.207.92.153.973-.247a1.561 1.561 0 0 0-.293-1.013c-.14-.26-.507-.167-.374-.487.114-.255.144-.54.087-.813a.828.828 0 0 0 .633-.44.541.541 0 0 0-.293-.593c-.44-.154-.8-.074-.893.206a.664.664 0 0 0 .08.467.793.793 0 0 0-.754.453.89.89 0 0 0-.446-.42c-.347.334-.7.66-1.06.98a.663.663 0 0 0 .506.5 1.139 1.139 0 0 0 .967-.526c.019.072.053.14.1.2a.66.66 0 0 0-.327.406zm3.42-5.393c.313.666.613.766 1.186.466.434-.22.874-.426 1.334-.633.533.84.666.867 1.18.327a.671.671 0 0 0-.494-.614.316.316 0 0 0 .107-.14.35.35 0 0 0-.005-.291.347.347 0 0 0-.222-.189.315.315 0 0 0-.128-.033.329.329 0 0 0-.318.2c-.44-.747-1.034-.72-1.687.067a1.397 1.397 0 0 1-.22-.067c-.233.3-.473.593-.713.887zm5.767 13.907a.61.61 0 0 0-.107-.727 2.728 2.728 0 0 0-.42-.353.52.52 0 0 0-.04-.094v-.233a.914.914 0 0 0-.58-.833 1.062 1.062 0 0 0-.534.019 1.063 1.063 0 0 0-.459.274.564.564 0 0 0-.14.38.814.814 0 0 0 .353.667c0 .326.2.573.607.633.047.003.093.003.14 0v.107c0 .346.127.473.46.513a.666.666 0 0 0 .72-.353zm2.213-2.447c-.167 0-.587.073-.614.24-.026.167.314.393.46.473.147.08.394-.113.414-.326.02-.214-.047-.387-.26-.387zm1.147-4.5c-.094.093-.294.207-.367.373-.073.167.127.514.407.554a.41.41 0 0 0 .308-.09.419.419 0 0 0 .152-.284.557.557 0 0 0-.5-.553zm-2.127 9.04c-.085.032-.154.096-.193.178s-.044.176-.013.262c.086.34.353.38.666.394.147-.26.36-.48.127-.74a.471.471 0 0 0-.587-.094zm1.467-1.733c-.087 0-.28-.053-.347.087-.066.14.12.26.167.26a.36.36 0 0 0 .267-.12s-.04-.214-.087-.227zM145.006 112c.153.06.347 0 .327-.22s-.06-.52-.227-.553c-.167-.034-.36.333-.38.466-.02.134.16.307.28.307zm.327 2.707a.386.386 0 0 0 0-.667c-.353-.12-.573.287-.5.487a.422.422 0 0 0 .5.18zm3.5-1.061a.485.485 0 0 0-.52.3.56.56 0 0 0 .307.594.436.436 0 0 0 .513-.314.486.486 0 0 0-.3-.58zm3.793-1.693a.508.508 0 0 0 0-.174l-.513-.52c-.32-.066-.527.04-.593.32-.1.4.119.88.433.94a.7.7 0 0 0 .673-.566zm.04 2.773a.413.413 0 0 0-.52.273.464.464 0 0 0 .456.555.404.404 0 0 0 .159-.042.385.385 0 0 0 .199-.253.476.476 0 0 0-.294-.533zm-1.999 1.641a.75.75 0 0 0-.53-.226.732.732 0 0 0-.53.226.672.672 0 0 0-.103.402c.01.142.065.276.156.385a.522.522 0 0 0 .7.16c.213-.1.667-.187.707-.527s-.274-.307-.4-.42zm-.801-5.28a.445.445 0 0 0-.387-.474.668.668 0 0 0-.667.52.67.67 0 0 0 .7.454.45.45 0 0 0 .274-.182.45.45 0 0 0 .08-.318zm-4.746-.42a.828.828 0 0 0 .847-.534c.066-.26-.214-.606-.547-.666-.433-.094-.667 0-.767.346a.783.783 0 0 0 .467.854zm3.546-1.334a.46.46 0 0 0 .54-.347.501.501 0 0 0-.413-.56.483.483 0 0 0-.52.38.52.52 0 0 0 .393.527zm-9.78 7.8a.75.75 0 0 0-.627.04.43.43 0 0 0-.353-.3c-.314-.06-.447.2-.594.453.06.087.121.187.181.267l.093.087a.359.359 0 0 0 .373.06l.087-.054a.804.804 0 0 0 .113.227.213.213 0 0 0 .107.147.73.73 0 0 0 .326.246.424.424 0 0 0 .177.019.422.422 0 0 0 .169-.052.427.427 0 0 0 .214-.273.665.665 0 0 0-.266-.867zm-1.093-1.679a.667.667 0 0 0 0-.167.999.999 0 0 0 .606-.433c.174-.267 0-.567-.04-.88a.56.56 0 0 0 .107-.207.478.478 0 0 0-.087-.353c-.033-.667-.613-.86-1.173-.86a.978.978 0 0 0-.593.206 1.261 1.261 0 0 0-.16.167.912.912 0 0 0-.487-.26c-.353-.067-.593.147-.667.593a.67.67 0 0 0 .427.667.916.916 0 0 0 .473-.107.785.785 0 0 0-.1.594 1.439 1.439 0 0 0 .874.886v.04c-.04.174.346.374.506.427s.3-.08.314-.313z"/> + <path fill="#0066b1" d="M135.86 112.5a1.065 1.065 0 0 0-.294-1.26.81.81 0 0 0-1.06.194.758.758 0 0 0 .12 1.173.894.894 0 0 0 1.234-.107zm-1.067-2.347a.842.842 0 0 0 .277-1.102.845.845 0 0 0-1.07-.384c-.313.113-.367.666-.227 1.093a.826.826 0 0 0 1.02.393zm-.467 3.98a.554.554 0 0 0-.2.093l.973.974c.035-.129.053-.261.054-.394a.822.822 0 0 0-.827-.673zm-1.306-4.993a.752.752 0 0 0-.582.024.735.735 0 0 0-.385.436c-.14.36.187.88.667 1.06a.657.657 0 0 0 .52-.038.658.658 0 0 0 .333-.402c.173-.64.013-.78-.553-1.08zM144 120.36c-.247-.14-.627.067-.813.44a.516.516 0 0 0 .22.627.668.668 0 0 0 .8-.2.748.748 0 0 0-.207-.867zm-11.334-9.4a.75.75 0 0 0-.833.277.755.755 0 0 0-.127.283.912.912 0 0 0 0 .266l1.054 1.047c.226-.033.32-.207.413-.387.066-.106.118-.22.153-.34a.705.705 0 0 0 .08-.166c.1-.427-.2-.84-.74-.98zm8.427 5.746a.832.832 0 0 0-.453-1 .58.58 0 0 0-.438.016.562.562 0 0 0-.295.324.847.847 0 0 0 .18.98.811.811 0 0 0 1.006-.32zm-9.807-7.326a.493.493 0 0 0-.166.354c0 .133-.074.253 0 .386.073.134.293.094.406-.073a.74.74 0 0 0 .087-.54.222.222 0 0 0-.13-.149.217.217 0 0 0-.197.022zm10.267 9.373a.7.7 0 0 0-.813.173.797.797 0 0 0 .326.94.724.724 0 0 0 .86-.36c.154-.306.027-.56-.373-.753zm-.173-5.593a.753.753 0 0 0-.84.433.512.512 0 0 0 .333.62.67.67 0 0 0 .754-.447.447.447 0 0 0 .006-.356.45.45 0 0 0-.253-.25zm-1.02-1.54a.418.418 0 0 0 .153-.054.426.426 0 0 0 .213-.286l.1-.114c.247.18.48.394.747.094a.475.475 0 0 0 .08-.607c-.053-.073-.16-.173-.233-.167l-.24.04a.475.475 0 0 0 .104-.348.455.455 0 0 0-.057-.177.443.443 0 0 0-.121-.141.68.68 0 0 0-.24-.135.674.674 0 0 0-.74.248.836.836 0 0 0-.173.373.44.44 0 0 0-.32.14.942.942 0 0 0 .167 1.1.398.398 0 0 0 .56.034zm-1.167 2.86c-.193.174-.393.367-.26.667a.405.405 0 0 0 .392.262.409.409 0 0 0 .161-.042.553.553 0 0 0 .294-.38c0-.32-.267-.467-.587-.507z"/> + <path fill="#294492" d="M175.172 25.74c1.935-2.095 2.189-5.01.566-6.509s-4.508-1.014-6.444 1.082c-1.936 2.097-2.189 5.011-.566 6.51 1.623 1.499 4.508 1.014 6.444-1.082z"/> + <path fill="#7de2d1" d="M155.779 18.4c.8 2.207 6.88 2.633 9.067 2.72a1.41 1.41 0 0 0 1.213-.607l.267-.387a1.395 1.395 0 0 0 .133-1.333c-.853-2-3.426-7.54-5.766-7.5-2.134.04-3.827 2.62-3.827 2.62s-1.813 2.487-1.087 4.487z"/> + <path fill="#7de2d1" d="M159.213 12.053c-.246 2.327 5.034 5.38 6.96 6.407a1.43 1.43 0 0 0 1.334 0l.406-.233a1.405 1.405 0 0 0 .754-1.167c.106-2.187.22-8.28-1.907-9.273-1.933-.9-4.587.666-4.587.666s-2.74 1.48-2.96 3.6z"/> + <path fill="#42d4c6" d="M160.666 11.274a3.212 3.212 0 0 0-1.406.38 2.713 2.713 0 0 0-.074.4c-.26 2.413 5.42 5.593 7.154 6.513-.907-2.167-3.38-7.333-5.674-7.293z"/> + <path fill="#7de2d1" d="M188.206 27.78c-.8-2.2-6.88-2.667-9.066-2.713a1.4 1.4 0 0 0-1.213.606l-.261.387a1.393 1.393 0 0 0-.14 1.333c.86 2 3.427 7.547 5.774 7.507 2.133-.04 3.826-2.62 3.826-2.62s1.807-2.493 1.08-4.5z"/> + <path fill="#7de2d1" d="M184.78 34.133c.247-2.333-5.04-5.38-6.967-6.413a1.406 1.406 0 0 0-1.333 0l-.407.226a1.394 1.394 0 0 0-.74 1.174c-.113 2.186-.22 8.286 1.907 9.273 1.933.9 4.587-.667 4.587-.667s2.726-1.473 2.953-3.593z"/> + <path fill="#42d4c6" d="M177.619 27.62c.933 2.16 3.413 7.333 5.68 7.293.492-.02.974-.151 1.407-.386.031-.13.056-.261.073-.394.253-2.413-5.447-5.6-7.16-6.513z"/> + <path fill="#00bfb3" d="M57.106 44.174H.333v7.433h56.773z"/> + <path fill="#ff957d" d="M85.16 61.326H25.147v7.433H85.16z"/> + <path fill="#fa744e" d="M63.333 68.76h21.833v-7.427h-19.96a42.007 42.007 0 0 0-1.873 7.427z"/> + <path fill="#294492" d="M153.753 43.7a1.624 1.624 0 0 1-1.59-2.016 1.62 1.62 0 0 1 .503-.831c.9-.787 1.833-1.627 2.767-2.487a1.632 1.632 0 0 1 2.206 2.4c-.96.88-1.906 1.734-2.826 2.534-.293.257-.67.4-1.06.4zm8.353-7.82a1.624 1.624 0 0 1-1.503-1.007 1.62 1.62 0 0 1 .356-1.773c1.6-1.594 2.594-2.667 2.6-2.667a1.628 1.628 0 0 1 2.36 2.233c-.04.04-1.033 1.087-2.666 2.714a1.64 1.64 0 0 1-1.147.5z"/> + <path fill="#f04e98" d="M54.22 21.241 41.506 33.955a3.807 3.807 0 0 0 0 5.383L54.22 52.052a3.807 3.807 0 0 0 5.383 0l12.714-12.714a3.807 3.807 0 0 0 0-5.383L59.603 21.24a3.807 3.807 0 0 0-5.383 0z"/> + <path fill="#01ada1" d="M31.247 47.473a.621.621 0 0 0-.367-.807c-.26-.126-.573.054-.74.42a.455.455 0 0 0 .193.667.974.974 0 0 0 .914-.28zm-10.027 3.76c-.053.107.12.254.2.307a.18.18 0 0 0 .227-.08.293.293 0 0 0-.074-.287c-.053-.04-.3-.046-.353.06zm2.36-4.853c.12.053.247-.133.293-.213a.234.234 0 0 0-.093-.24.446.446 0 0 0-.273.146c-.06.074-.04.254.073.307zm2.453 3.88c-.26-.04-.52-.12-.773-.18a.38.38 0 0 0-.607.113.5.5 0 0 0 .194.667c.143.089.266.207.36.347a.732.732 0 0 0 .446.393h.287a.968.968 0 0 0 .473-.767c-.02-.246-.026-.52-.38-.573zm1.18-2.694a.301.301 0 0 0-.453.214c.006.144.035.285.086.42a.46.46 0 0 0 .594-.28c.06-.207-.074-.294-.227-.354zm5.713-.366a.46.46 0 0 0 .62-.193.775.775 0 0 0 0-.12.42.42 0 0 0 .267-.273.407.407 0 0 0-.14-.34.506.506 0 0 0-.414 0 .366.366 0 0 0-.093.113.52.52 0 0 0-.527.26.46.46 0 0 0 .287.553zm-3.54 3.734a.426.426 0 0 0-.553.2.414.414 0 0 0 .04.473h.746a.377.377 0 0 0 .074-.107.452.452 0 0 0-.307-.566zM29.2 49.02c.06-.207-.074-.287-.227-.354-.153-.066-.433 0-.453.214.004.144.034.286.086.42a.447.447 0 0 0 .594-.28zM27.186 46a.26.26 0 0 0 .094.3c.1.046.24-.054.28-.12.04-.067.046-.327-.054-.367s-.293.093-.32.187zm-3.939 1.56c-.374-.04-.48.447-.354.667s.054.173-.093.413-.22.667.26.773a.934.934 0 0 0 1.033-.666 1.086 1.086 0 0 0-.846-1.187zm20.326-.86c-.287-.667-.827-.273-.973-.447-.147-.173-.18-.633-.374-.913a.733.733 0 0 0-1.106-.253.807.807 0 0 0-.134 1.18.398.398 0 0 0-.04.073.88.88 0 0 0 .567.82.786.786 0 0 0 .4 0 .52.52 0 0 0 0 .507c.227.573.86.726 1.113.366.254-.36.074-.666.154-.753.08-.087.52-.26.393-.58zm1.527.78a.555.555 0 0 0-.18 0 .48.48 0 0 0-.313-.374.72.72 0 0 0-.747.487.394.394 0 0 0 .32.493c.1.03.207.03.307 0a.533.533 0 0 0 .2.507c.426.233.786-.127.966-.32s-.146-.707-.553-.793zm.607-2.86a.667.667 0 0 0-.754-.353c-.12 0-.233.04-.36.06l-.12-.153h-1.7c0 .04.06.08.094.113a.84.84 0 0 0 .14.247c-.134.46.133.707.5.907-.094.24-.32.466 0 .706a.42.42 0 0 0 .553 0c.3-.226.127-.486 0-.74l.353-.326.447.44c.165-.03.325-.077.48-.14a.628.628 0 0 0 .22-.234.667.667 0 0 0 .207-.233.301.301 0 0 0-.06-.294zm-10.994 2.353a.46.46 0 0 0 0-.773.627.627 0 0 0-.546 0 .52.52 0 0 0-.194.446.534.534 0 0 0 .74.327zm6.267-2.206c.087-.14-.053-.487-.24-.593h-.22a.42.42 0 0 0-.253.393c.04.247.606.367.713.2zm-2.76 2.813c-.127.36 0 .666.246.766a.913.913 0 0 0 1.014-.453.667.667 0 0 0-.454-.667.574.574 0 0 0-.806.354zm-2.82-3.406h-.553c.046.133.1.24.16.267s.326-.067.393-.267zm3.12 2.833c.44-.567.707-.133 1.153-.433a.852.852 0 0 0 .16-.187.58.58 0 0 0-.533-.907c-.323.008-.644.06-.953.154a.9.9 0 0 0-.494.94c.034.206.274.606.667.433zm.98-1.674a.32.32 0 0 0 .46-.233.6.6 0 0 0-.107-.447c-.093-.087-.533-.047-.607.14a.426.426 0 0 0 .254.54zm-7.927.507c.26-.08.293-.294.18-.787-.253-.047-.54-.193-.727.14a.433.433 0 0 0 .12.587.506.506 0 0 0 .427.06zm16.58 4.413a.447.447 0 0 0-.6.247.4.4 0 0 0 .287.513.46.46 0 0 0 .58-.293.362.362 0 0 0-.13-.405.36.36 0 0 0-.137-.062zm-3.1-.573a.248.248 0 0 0-.1 0 .867.867 0 0 0-.467-.453.707.707 0 0 0-.773.533.43.43 0 0 0 0 .16 1.44 1.44 0 0 0-.094.56c.047.127.287.4.067.554-.22.153-.56.14-.667.406l-.04.094a.38.38 0 0 0-.173-.1.667.667 0 0 0-.487.146h1.914a.606.606 0 0 1 0-.326c.073-.154.213-.3.433-.167.22.133.62.053.867-.42.246-.473-.094-1.04-.48-.987zm2.1 1.047c-.227-.06-.58.233-.627.52a.333.333 0 0 0 .16.36h.873a.266.266 0 0 0 .074-.127.74.74 0 0 0-.48-.753zm-1.693.879h1.133a.947.947 0 0 0-.326-.2.666.666 0 0 0-.807.2zm2.253-2.213c.12-.453-.114-.787-.667-.913a1.168 1.168 0 0 0-.36 0 .48.48 0 0 0-.447.227.96.96 0 0 0-.173.34.818.818 0 0 0 .74.953.834.834 0 0 0 .907-.607zm3.9.321a.267.267 0 0 0 .227-.054l-1.073-1.073a1.08 1.08 0 0 0-.274.373.6.6 0 0 0 .354.834.714.714 0 0 0 .766-.08zm-2.533 1.48a.474.474 0 0 0-.454.413h.893a.367.367 0 0 0-.44-.413zm3.493-.794c-.4-.053-.893.033-1.04.267a.194.194 0 0 0-.04.113c.113.38-.187.52-.393.727a.48.48 0 0 0-.074.1h1.247a.43.43 0 0 1 .28-.2c.067.06.133.146.2.2h.58a.76.76 0 0 0 .113-.04.254.254 0 0 0 .1-.22z"/> + <path fill="#01ada1" d="M49.18 47.413a.83.83 0 0 0-.247-.08.666.666 0 0 0 .22 0 .56.56 0 0 0 .226-.113l-1.533-1.533c-.087.186-.16.346-.233.36-.074.013-.667-.487-1.187-.28a.94.94 0 0 0-.507 1.193 1.58 1.58 0 0 0 1.48.826c.427-.113.44-.666.56-.713s.474.253.887.287a.746.746 0 0 0-.667.427.907.907 0 0 0-.073.546.726.726 0 0 0-.113.24.975.975 0 0 0 .813 1.154.94.94 0 0 0 1.147-.714.748.748 0 0 0-.107-.666.986.986 0 0 0-.667-.934zM42.14 50a.087.087 0 0 1 0-.04.5.5 0 0 0 .38-.38.46.46 0 0 0-.42-.493.433.433 0 0 0-.247 0 .833.833 0 0 0-.927.34.827.827 0 0 0-.32-.134.494.494 0 0 0-.667.294.954.954 0 0 0 .467.893.566.566 0 0 0 .667-.453c.066.058.14.108.22.146a.591.591 0 0 0 .846-.173zm-6.06 1.606h.7a.808.808 0 0 0-.2-.147.374.374 0 0 0-.5.147zm.36-2.379c.113-.293 0-.707-.213-.793a.86.86 0 0 0-.6.04.433.433 0 0 0-.26-.167.473.473 0 0 0-.514.26 1.566 1.566 0 0 0-.853-.12.668.668 0 0 1-.367-.067.586.586 0 0 0-.966.287.339.339 0 0 0-.14-.1.526.526 0 0 0-.554.38.54.54 0 0 0 .247.72.454.454 0 0 0 .573-.307c.12.096.262.16.414.187a.88.88 0 0 1 .58.26.934.934 0 0 0 .893.22.813.813 0 0 0 .62-.667v-.086a.76.76 0 0 0 .407.406c.293.114.573-.073.733-.453zm1.227-4.56a.58.58 0 0 0-1.054.053c-.117.24-.204.492-.26.753a.114.114 0 0 0-.093-.093.14.14 0 0 0-.147.113c0 .04.074.12.107.127a.14.14 0 0 0 .1-.033.92.92 0 0 0 .607.927.36.36 0 0 0 .473-.147c.27-.443.397-.957.367-1.474a1.003 1.003 0 0 0-.1-.226zm-2.054 6.539a.507.507 0 0 0-.666.287.313.313 0 0 0 0 .113h.94a.453.453 0 0 0-.274-.4zm3.433-1.346A.813.813 0 0 0 38 51.074a.42.42 0 0 0-.094.18.526.526 0 0 0 0 .353h.76v-.04a.473.473 0 0 0 0-.193.948.948 0 0 0 .667-.534.76.76 0 0 0-.287-.98zm3.407 1.513c.08-.187-.06-.4-.167-.533-.107-.134-.387 0-.473.16s0 .42.153.466c.153.047.407.087.487-.093zm-11.067-.566c-.353-.133-.746 0-.84.22a.787.787 0 0 0 .107.58h1.127a.574.574 0 0 0-.394-.8zm8.527.8h.533a.554.554 0 0 0-.2-.094.406.406 0 0 0-.333.094zm.087-2.78a.454.454 0 0 0-.267-.313c-.113 0-.433 0-.487.146-.053.147.2.367.314.434.113.066.44-.08.44-.267z"/> + <path fill="#fff" d="M58.54 39.7h-3.66l-1.22-11.393h6.1zm.406 5.083a2.237 2.237 0 1 0-4.473 0 2.237 2.237 0 0 0 4.473 0z"/> +</svg> diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx index 31ae68244e982..208b1e576c0d7 100644 --- a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { LegendAction } from './legend_action'; -import { MetricTypes, MetricSeries } from '../../../common/rest_types'; +import { type MetricTypes, type MetricSeries } from '../../../common/rest_types'; import { formatBytes } from '../../utils/format_bytes'; // TODO: Remove this when we have a title for each metric type @@ -74,6 +74,9 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({ }, [series]); const renderLegendAction = useCallback( ({ label }: { label: string }) => { + if (label === 'Total') { + return null; + } return ( <LegendAction label={label} diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx index 4a2b9b4fbc875..271cfe432402d 100644 --- a/x-pack/plugins/data_usage/public/app/components/charts.tsx +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -6,9 +6,8 @@ */ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup } from '@elastic/eui'; -import { MetricTypes } from '../../../common/rest_types'; import { ChartPanel } from './chart_panel'; -import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types'; +import type { UsageMetricsResponseSchemaBody, MetricTypes } from '../../../common/rest_types'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; interface ChartsProps { data: UsageMetricsResponseSchemaBody; diff --git a/x-pack/plugins/data_usage/public/app/components/charts_loading.tsx b/x-pack/plugins/data_usage/public/app/components/charts_loading.tsx new file mode 100644 index 0000000000000..08c37934c451e --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/charts_loading.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiLoadingChart } from '@elastic/eui'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; + +export const ChartsLoading = ({ + 'data-test-subj': dataTestSubj, +}: { + 'data-test-subj'?: string; +}) => { + const getTestId = useTestIdGenerator(dataTestSubj); + // returns 2 loading icons for the two charts + return ( + <EuiFlexGroup + direction="column" + alignItems="center" + data-test-subj={getTestId('charts-loading')} + > + {[...Array(2)].map((i) => ( + <EuiFlexItem key={i}> + <EuiPanel paddingSize="xl" hasShadow={false} hasBorder={false}> + <EuiLoadingChart size="l" /> + </EuiPanel> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); +}; + +ChartsLoading.displayName = 'ChartsLoading'; diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.test.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.test.tsx index 8ece65b7a57a4..91e2fd5ddafa9 100644 --- a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.test.tsx +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { TestProvider } from '../../../common/test_utils'; +import { render, waitFor, within, type RenderResult } from '@testing-library/react'; import userEvent, { type UserEvent } from '@testing-library/user-event'; import { DataUsageMetrics } from './data_usage_metrics'; import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics'; @@ -102,21 +103,6 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { to: 'now', display: 'Last 7 days', }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - }, ], }; return x[k]; @@ -156,6 +142,7 @@ describe('DataUsageMetrics', () => { let user: UserEvent; const testId = 'test'; const testIdFilter = `${testId}-filter`; + let renderComponent: () => RenderResult; beforeAll(() => { jest.useFakeTimers(); @@ -167,18 +154,24 @@ describe('DataUsageMetrics', () => { beforeEach(() => { jest.clearAllMocks(); + renderComponent = () => + render( + <TestProvider> + <DataUsageMetrics data-test-subj={testId} /> + </TestProvider> + ); user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 }); mockUseGetDataUsageMetrics.mockReturnValue(getBaseMockedDataUsageMetrics); mockUseGetDataUsageDataStreams.mockReturnValue(getBaseMockedDataStreams); }); it('renders', () => { - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); expect(getByTestId(`${testId}`)).toBeTruthy(); }); it('should show date filter', () => { - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); const dateFilter = getByTestId(`${testIdFilter}-date-range`); expect(dateFilter).toBeTruthy(); expect(dateFilter.textContent).toContain('to'); @@ -190,12 +183,12 @@ describe('DataUsageMetrics', () => { ...getBaseMockedDataStreams, isFetching: true, }); - const { queryByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { queryByTestId } = renderComponent(); expect(queryByTestId(`${testIdFilter}-dataStreams-popoverButton`)).not.toBeTruthy(); }); it('should show data streams filter', () => { - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy(); }); @@ -205,7 +198,7 @@ describe('DataUsageMetrics', () => { data: generateDataStreams(5), isFetching: false, }); - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( 'Data streams5' ); @@ -217,29 +210,76 @@ describe('DataUsageMetrics', () => { data: generateDataStreams(100), isFetching: false, }); - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`); expect(toggleFilterButton).toHaveTextContent('Data streams50'); }); - it('should allow de-selecting all but one data stream option', async () => { + it('should allow de-selecting data stream options', async () => { mockUseGetDataUsageDataStreams.mockReturnValue({ error: undefined, - data: generateDataStreams(5), + data: generateDataStreams(10), isFetching: false, }); - const { getByTestId, getAllByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId, getAllByTestId } = renderComponent(); const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`); - expect(toggleFilterButton).toHaveTextContent('Data streams5'); + expect(toggleFilterButton).toHaveTextContent('Data streams10'); await user.click(toggleFilterButton); const allFilterOptions = getAllByTestId('dataStreams-filter-option'); - for (let i = 0; i < allFilterOptions.length - 1; i++) { + // deselect 9 options + for (let i = 0; i < allFilterOptions.length; i++) { await user.click(allFilterOptions[i]); } expect(toggleFilterButton).toHaveTextContent('Data streams1'); + expect(within(toggleFilterButton).getByRole('marquee').getAttribute('aria-label')).toEqual( + '1 active filters' + ); + }); + + it('should allow selecting/deselecting all data stream options using `select all` and `clear all`', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + error: undefined, + data: generateDataStreams(10), + isFetching: false, + }); + const { getByTestId } = renderComponent(); + const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`); + + expect(toggleFilterButton).toHaveTextContent('Data streams10'); + await user.click(toggleFilterButton); + + // all options are selected on load + expect(within(toggleFilterButton).getByRole('marquee').getAttribute('aria-label')).toEqual( + '10 active filters' + ); + + const selectAllButton = getByTestId(`${testIdFilter}-dataStreams-selectAllButton`); + const clearAllButton = getByTestId(`${testIdFilter}-dataStreams-clearAllButton`); + + // select all is disabled + expect(selectAllButton).toBeTruthy(); + expect(selectAllButton.getAttribute('disabled')).not.toBeNull(); + + // clear all is enabled + expect(clearAllButton).toBeTruthy(); + expect(clearAllButton.getAttribute('disabled')).toBeNull(); + // click clear all and expect all options to be deselected + await user.click(clearAllButton); + expect(within(toggleFilterButton).getByRole('marquee').getAttribute('aria-label')).toEqual( + '10 available filters' + ); + // select all is enabled again + expect(await selectAllButton.getAttribute('disabled')).toBeNull(); + // click select all + await user.click(selectAllButton); + + // all options are selected and clear all is disabled + expect(within(toggleFilterButton).getByRole('marquee').getAttribute('aria-label')).toEqual( + '10 active filters' + ); }); it('should not call usage metrics API if no data streams', async () => { @@ -247,7 +287,7 @@ describe('DataUsageMetrics', () => { ...getBaseMockedDataStreams, data: [], }); - render(<DataUsageMetrics data-test-subj={testId} />); + renderComponent(); expect(mockUseGetDataUsageMetrics).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ enabled: false }) @@ -259,7 +299,7 @@ describe('DataUsageMetrics', () => { ...getBaseMockedDataUsageMetrics, isFetching: true, }); - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); expect(getByTestId(`${testId}-charts-loading`)).toBeTruthy(); }); @@ -290,10 +330,19 @@ describe('DataUsageMetrics', () => { ], }, }); - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); expect(getByTestId(`${testId}-charts`)).toBeTruthy(); }); + it('should show no charts callout', () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: false, + }); + const { getByTestId } = renderComponent(); + expect(getByTestId(`${testId}-no-charts-callout`)).toBeTruthy(); + }); + it('should refetch usage metrics with `Refresh` button click', async () => { const refetch = jest.fn(); mockUseGetDataUsageMetrics.mockReturnValue({ @@ -306,7 +355,7 @@ describe('DataUsageMetrics', () => { isFetched: true, refetch, }); - const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />); + const { getByTestId } = renderComponent(); const refreshButton = getByTestId(`${testIdFilter}-super-refresh-button`); // click refresh 5 times for (let i = 0; i < 5; i++) { @@ -326,7 +375,7 @@ describe('DataUsageMetrics', () => { isFetched: true, error: new Error('Uh oh!'), }); - render(<DataUsageMetrics data-test-subj={testId} />); + renderComponent(); await waitFor(() => { expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ title: 'Error getting usage metrics', @@ -341,7 +390,7 @@ describe('DataUsageMetrics', () => { isFetched: true, error: new Error('Uh oh!'), }); - render(<DataUsageMetrics data-test-subj={testId} />); + renderComponent(); await waitFor(() => { expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ title: 'Error getting data streams', diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx index 5460c7ada0389..d7d6417cf1444 100644 --- a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx @@ -7,18 +7,20 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Charts } from './charts'; import { useBreadcrumbs } from '../../utils/use_breadcrumbs'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { PLUGIN_NAME } from '../../../common'; +import { DEFAULT_METRIC_TYPES, type UsageMetricsRequestBody } from '../../../common/rest_types'; +import { PLUGIN_NAME } from '../../translations'; import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics'; import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker'; -import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types'; import { ChartFilters, ChartFiltersProps } from './filters/charts_filters'; +import { ChartsLoading } from './charts_loading'; +import { NoDataCallout } from './no_data_callout'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; const EuiItemCss = css` @@ -33,6 +35,8 @@ export const DataUsageMetrics = memo( ({ 'data-test-subj': dataTestSubj = 'data-usage-metrics' }: { 'data-test-subj'?: string }) => { const getTestId = useTestIdGenerator(dataTestSubj); + const [isFirstPageLoad, setIsFirstPageLoad] = useState(true); + const { services: { chrome, appParams, notifications }, } = useKibanaContextForPlugin(); @@ -68,10 +72,10 @@ export const DataUsageMetrics = memo( }); useEffect(() => { - if (!metricTypesFromUrl) { + if (!metricTypesFromUrl && isFirstPageLoad) { setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); } - if (!dataStreamsFromUrl && dataStreams) { + if (!dataStreamsFromUrl && dataStreams && isFirstPageLoad) { const hasMoreThan50 = dataStreams.length > 50; const _dataStreams = hasMoreThan50 ? dataStreams.slice(0, 50) : dataStreams; setUrlDataStreamsFilter(_dataStreams.map((ds) => ds.name).join(',')); @@ -83,6 +87,7 @@ export const DataUsageMetrics = memo( dataStreams, dataStreamsFromUrl, endDateFromUrl, + isFirstPageLoad, metricTypesFromUrl, metricsFilters.dataStreams, metricsFilters.from, @@ -106,9 +111,9 @@ export const DataUsageMetrics = memo( const { error: errorFetchingDataUsageMetrics, - data, + data: usageMetricsData, isFetching, - isFetched, + isFetched: hasFetchedDataUsageMetricsData, refetch: refetchDataUsageMetrics, } = useGetDataUsageMetrics( { @@ -118,10 +123,16 @@ export const DataUsageMetrics = memo( }, { retry: false, - enabled: !!metricsFilters.dataStreams.length, + enabled: !!(metricsFilters.dataStreams.length && metricsFilters.metricTypes.length), } ); + useEffect(() => { + if (!isFetching && hasFetchedDataUsageMetricsData) { + setIsFirstPageLoad(false); + } + }, [isFetching, hasFetchedDataUsageMetricsData]); + const onRefresh = useCallback(() => { refetchDataUsageMetrics(); }, [refetchDataUsageMetrics]); @@ -204,13 +215,14 @@ export const DataUsageMetrics = memo( data-test-subj={getTestId('filter')} /> </FlexItemWithCss> - <FlexItemWithCss> - {isFetched && data ? ( - <Charts data={data} data-test-subj={dataTestSubj} /> + {hasFetchedDataUsageMetricsData && usageMetricsData ? ( + <Charts data={usageMetricsData} data-test-subj={dataTestSubj} /> ) : isFetching ? ( - <EuiLoadingElastic data-test-subj={getTestId('charts-loading')} /> - ) : null} + <ChartsLoading data-test-subj={dataTestSubj} /> + ) : ( + <NoDataCallout data-test-subj={dataTestSubj} /> + )} </FlexItemWithCss> </EuiFlexGroup> ); diff --git a/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx index d6627f3d8dca2..8e81e6091156b 100644 --- a/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx +++ b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx @@ -6,13 +6,14 @@ */ import React from 'react'; -import { EuiListGroupItem } from '@elastic/eui'; import { DataQualityDetailsLocatorParams, DATA_QUALITY_DETAILS_LOCATOR_ID, } from '@kbn/deeplinks-observability'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; import { useDateRangePicker } from '../hooks/use_date_picker'; +import { LegendActionItem } from './legend_action_item'; +import { UX_LABELS } from '../../translations'; interface DatasetQualityLinkProps { dataStreamName: string; @@ -39,6 +40,8 @@ export const DatasetQualityLink: React.FC<DatasetQualityLinkProps> = React.memo( await locator.navigate(locatorParams); } }; - return <EuiListGroupItem label="View data quality" onClick={onClickDataQuality} />; + return ( + <LegendActionItem label={UX_LABELS.dataQualityPopup.view} onClick={onClickDataQuality} /> + ); } ); diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx index 6b4806537e74b..fcff6fc13f260 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx @@ -5,18 +5,15 @@ * 2.0. */ -import { orderBy } from 'lodash/fp'; +import { orderBy, findKey } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; +import { EuiPopoverTitle, EuiSelectable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; -import { - METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, - type MetricTypes, -} from '../../../../common/rest_types'; - -import { UX_LABELS } from '../../translations'; +import { METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP } from '../../../../common/rest_types'; +import { UX_LABELS } from '../../../translations'; import { ChartsFilterPopover } from './charts_filter_popover'; +import { ToggleAllButton } from './toggle_all_button'; import { FilterItems, FilterName, useChartsFilter } from '../../hooks'; const getSearchPlaceholder = (filterName: FilterName) => { @@ -84,6 +81,11 @@ export const ChartsFilter = memo<ChartsFilterProps>( }, }); + const addHeightToPopover = useMemo( + () => isDataStreamsFilter && numFilters + numActiveFilters > 15, + [isDataStreamsFilter, numFilters, numActiveFilters] + ); + // track popover state to pin selected options const wasPopoverOpen = useRef(isPopoverOpen); @@ -102,7 +104,7 @@ export const ChartsFilter = memo<ChartsFilterProps>( ); // augmented options based on the dataStreams filter - const sortedHostsFilterOptions = useMemo(() => { + const sortedDataStreamsFilterOptions = useMemo(() => { if (shouldPinSelectedDataStreams() || areDataStreamsSelectedOnMount) { // pin checked items to the top return orderBy('checked', 'asc', items); @@ -116,12 +118,6 @@ export const ChartsFilter = memo<ChartsFilterProps>( const onOptionsChange = useCallback( (newOptions: FilterItems) => { const optionItemsToSet = newOptions.map((option) => option); - const currChecks = optionItemsToSet.filter((option) => option.checked === 'on'); - - // don't update filter state if trying to uncheck all options - if (currChecks.length < 1) { - return; - } // update filter UI options state setItems(optionItemsToSet); @@ -136,13 +132,9 @@ export const ChartsFilter = memo<ChartsFilterProps>( // update URL params if (isMetricsFilter) { - setUrlMetricTypesFilter( - selectedItems - .map((item) => METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[item as MetricTypes]) - .join() - ); + setUrlMetricTypesFilter(selectedItems.join(',')); } else if (isDataStreamsFilter) { - setUrlDataStreamsFilter(selectedItems.join()); + setUrlDataStreamsFilter(selectedItems.join(',')); } // reset shouldPinSelectedDataStreams, setAreDataStreamsSelectedOnMount shouldPinSelectedDataStreams(false); @@ -162,6 +154,63 @@ export const ChartsFilter = memo<ChartsFilterProps>( ] ); + const onSelectAll = useCallback(() => { + const allItems: FilterItems = items.map((item) => { + return { + ...item, + checked: 'on', + }; + }); + setItems(allItems); + const optionsToSelect = allItems.map((i) => i.label); + onChangeFilterOptions(optionsToSelect); + + if (isDataStreamsFilter) { + setUrlDataStreamsFilter(optionsToSelect.join(',')); + } + if (isMetricsFilter) { + setUrlMetricTypesFilter( + optionsToSelect + .map((option) => findKey(METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, option)) + .join(',') + ); + } + }, [ + items, + isDataStreamsFilter, + isMetricsFilter, + setItems, + onChangeFilterOptions, + setUrlDataStreamsFilter, + setUrlMetricTypesFilter, + ]); + + const onClearAll = useCallback(() => { + setItems( + items.map((item) => { + return { + ...item, + checked: undefined, + }; + }) + ); + onChangeFilterOptions([]); + if (isDataStreamsFilter) { + setUrlDataStreamsFilter(''); + } + if (isMetricsFilter) { + setUrlMetricTypesFilter(''); + } + }, [ + items, + isDataStreamsFilter, + isMetricsFilter, + setItems, + onChangeFilterOptions, + setUrlDataStreamsFilter, + setUrlMetricTypesFilter, + ]); + useEffect(() => { return () => { wasPopoverOpen.current = isPopoverOpen; @@ -182,9 +231,10 @@ export const ChartsFilter = memo<ChartsFilterProps>( <EuiSelectable aria-label={`${filterName}`} emptyMessage={UX_LABELS.filterEmptyMessage(filterName)} + height={addHeightToPopover ? 380 : undefined} isLoading={isFilterLoading} onChange={onOptionsChange} - options={sortedHostsFilterOptions} + options={sortedDataStreamsFilterOptions} searchable={isSearchable ? true : undefined} searchProps={{ placeholder: getSearchPlaceholder(filterName), @@ -203,6 +253,28 @@ export const ChartsFilter = memo<ChartsFilterProps>( </EuiPopoverTitle> )} {list} + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem grow={1}> + <ToggleAllButton + color="primary" + data-test-subj={getTestId(`${filterName}-selectAllButton`)} + icon="check" + label={UX_LABELS.filterSelectAll} + isDisabled={hasActiveFilters && numFilters === 0} + onClick={onSelectAll} + /> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <ToggleAllButton + color="danger" + data-test-subj={getTestId(`${filterName}-clearAllButton`)} + icon="cross" + label={UX_LABELS.filterClearAll} + isDisabled={!hasActiveFilters} + onClick={onClearAll} + /> + </EuiFlexItem> + </EuiFlexGroup> </div> ); }} diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx index 3c0237c84a0c9..a2f4585e592ce 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx @@ -9,7 +9,7 @@ import React, { memo, useMemo } from 'react'; import { EuiFilterButton, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; import { type FilterName } from '../../hooks/use_charts_filter'; -import { FILTER_NAMES } from '../../translations'; +import { FILTER_NAMES } from '../../../translations'; export const ChartsFilterPopover = memo( ({ diff --git a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx index 62c6cc542a523..81ab435670f89 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx @@ -15,8 +15,9 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import moment from 'moment'; +import { momentDateParser } from '../../../../common/utils'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { DEFAULT_DATE_RANGE_OPTIONS } from '../../hooks/use_date_picker'; export interface DateRangePickerValues { autoRefreshOptions: { @@ -50,16 +51,23 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps const kibana = useKibana<IUnifiedSearchPluginServices>(); const { uiSettings } = kibana.services; const [commonlyUsedRanges] = useState(() => { - return ( - uiSettings - ?.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) - ?.map(({ from, to, display }: { from: string; to: string; display: string }) => { - return { + const _commonlyUsedRanges: Array<{ from: string; to: string; display: string }> = + uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES); + if (!_commonlyUsedRanges) { + return []; + } + return _commonlyUsedRanges.reduce<DurationRange[]>( + (acc, { from, to, display }: { from: string; to: string; display: string }) => { + if (!['now-30d/d', 'now-90d/d', 'now-1y/d'].includes(from)) { + acc.push({ start: from, end: to, label: display, - }; - }) ?? [] + }); + } + return acc; + }, + [] ); }); @@ -80,9 +88,9 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps showUpdateButton={false} timeFormat={'HH:mm'} updateButtonProps={{ iconOnly: false, fill: false }} - utcOffset={moment().utcOffset() / 60} - maxDate={moment()} - minDate={moment().subtract(9, 'days').startOf('day')} + utcOffset={0} + maxDate={momentDateParser(DEFAULT_DATE_RANGE_OPTIONS.maxDate)} + minDate={momentDateParser(DEFAULT_DATE_RANGE_OPTIONS.minDate)} width="auto" /> ); diff --git a/x-pack/plugins/data_usage/public/app/components/filters/toggle_all_button.tsx b/x-pack/plugins/data_usage/public/app/components/filters/toggle_all_button.tsx new file mode 100644 index 0000000000000..3d1c4080fcc9c --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/filters/toggle_all_button.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 { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import React, { memo } from 'react'; +import { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui'; + +const EuiButtonEmptyCss = css` + border-top: ${euiThemeVars.euiBorderThin}; + border-radius: 0; +`; + +interface ToggleAllButtonProps { + 'data-test-subj'?: string; + color: EuiButtonEmptyProps['color']; + icon: EuiButtonEmptyProps['iconType']; + isDisabled: boolean; + onClick: () => void; + label: string; +} + +export const ToggleAllButton = memo<ToggleAllButtonProps>( + ({ color, 'data-test-subj': dataTestSubj, icon, isDisabled, label, onClick }) => { + // const getTestId = useTestIdGenerator(dataTestSubj); + return ( + <EuiButtonEmpty + color={color} + css={EuiButtonEmptyCss} + data-test-subj={dataTestSubj} + iconType={icon} + isDisabled={isDisabled} + onClick={onClick} + > + {label} + </EuiButtonEmpty> + ); + } +); + +ToggleAllButton.displayName = 'ToggleAllButton'; diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx index c9059037c4445..b748b77163245 100644 --- a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx +++ b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx @@ -5,18 +5,12 @@ * 2.0. */ import React, { useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiPopover, - EuiListGroup, - EuiListGroupItem, - EuiSpacer, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiPopover, EuiListGroup } from '@elastic/eui'; import { IndexManagementLocatorParams } from '@kbn/index-management-shared-types'; import { DatasetQualityLink } from './dataset_quality_link'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; +import { LegendActionItem } from './legend_action_item'; +import { UX_LABELS } from '../../translations'; interface LegendActionProps { idx: number; @@ -63,7 +57,7 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo( <EuiFlexItem grow={false}> <EuiButtonIcon iconType="boxesHorizontal" - aria-label="Open data stream actions" + aria-label={UX_LABELS.dataQualityPopup.open} onClick={() => togglePopover(uniqueStreamName)} /> </EuiFlexItem> @@ -74,11 +68,15 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo( anchorPosition="downRight" > <EuiListGroup gutterSize="none"> - <EuiListGroupItem label="Copy data stream name" onClick={onCopyDataStreamName} /> - <EuiSpacer size="s" /> - + <LegendActionItem + label={UX_LABELS.dataQualityPopup.copy} + onClick={onCopyDataStreamName} + /> {hasIndexManagementFeature && ( - <EuiListGroupItem label="Manage data stream" onClick={onClickIndexManagement} /> + <LegendActionItem + label={UX_LABELS.dataQualityPopup.manage} + onClick={onClickIndexManagement} + /> )} {hasDataSetQualityFeature && <DatasetQualityLink dataStreamName={label} />} </EuiListGroup> diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx new file mode 100644 index 0000000000000..3b4f0d9f698f7 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiListGroupItem } from '@elastic/eui'; + +export const LegendActionItem = memo( + ({ label, onClick }: { label: string; onClick: () => Promise<void> | void }) => ( + <EuiListGroupItem label={label} onClick={onClick} size="s" /> + ) +); + +LegendActionItem.displayName = 'LegendActionItem'; diff --git a/x-pack/plugins/data_usage/public/app/components/no_data_callout.tsx b/x-pack/plugins/data_usage/public/app/components/no_data_callout.tsx new file mode 100644 index 0000000000000..c8c06db351060 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/no_data_callout.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import icon from './assets/illustration_product_no_results_magnifying_glass.svg'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; + +export const NoDataCallout = ({ + 'data-test-subj': dateTestSubj, +}: { + 'data-test-subj'?: string; +}) => { + const getTestId = useTestIdGenerator(dateTestSubj); + + return ( + <EuiFlexGroup + style={{ height: 490 }} + alignItems="center" + justifyContent="center" + data-test-subj={getTestId('no-charts-callout')} + > + <EuiFlexItem grow={false}> + <EuiPanel hasBorder={true} style={{ maxWidth: 500 }}> + <EuiFlexGroup> + <EuiFlexItem> + <EuiText size="s"> + <EuiTitle> + <h3> + <FormattedMessage + id="xpack.dataUsage.noCharts.title" + defaultMessage="No chart data without data streams" + /> + </h3> + </EuiTitle> + <p> + <FormattedMessage + id="xpack.dataUsage.noCharts.description" + defaultMessage="Try searching with at least one data stream." + /> + </p> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiImage style={{ width: 200, height: 148 }} size="200" alt="" url={icon} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +NoDataCallout.displayName = 'NoDataCallout'; diff --git a/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx b/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx index 69edb7a7f01ce..adc53e12b5749 100644 --- a/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx +++ b/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { DataUsagePage } from './components/page'; -import { DATA_USAGE_PAGE } from './translations'; +import { DATA_USAGE_PAGE } from '../translations'; import { DataUsageMetrics } from './components/data_usage_metrics'; export const DataUsageMetricsPage = () => { diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx index d2c5dc554ff2d..012a6027aadb2 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx @@ -6,13 +6,13 @@ */ import { useState, useEffect, useMemo } from 'react'; +import { DEFAULT_SELECTED_OPTIONS } from '../../../common'; import { - isDefaultMetricType, - METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, METRIC_TYPE_VALUES, + METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, + isDefaultMetricType, } from '../../../common/rest_types'; -import { DEFAULT_SELECTED_OPTIONS } from '../../../common'; -import { FILTER_NAMES } from '../translations'; +import { FILTER_NAMES } from '../../translations'; import { useDataUsageMetricsUrlParams } from './use_charts_url_params'; import { formatBytes } from '../../utils/format_bytes'; import { ChartsFilterProps } from '../components/filters/charts_filter'; @@ -48,6 +48,7 @@ export const useChartsFilter = ({ } => { const { dataStreams: selectedDataStreamsFromUrl, + metricTypes: selectedMetricTypesFromUrl, setUrlMetricTypesFilter, setUrlDataStreamsFilter, } = useDataUsageMetricsUrlParams(); @@ -73,8 +74,13 @@ export const useChartsFilter = ({ ? METRIC_TYPE_VALUES.map((metricType) => ({ key: metricType, label: METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[metricType], - checked: isDefaultMetricType(metricType) ? 'on' : undefined, // default metrics are selected by default - disabled: isDefaultMetricType(metricType), + checked: selectedMetricTypesFromUrl + ? selectedMetricTypesFromUrl.includes(metricType) + ? 'on' + : undefined + : isDefaultMetricType(metricType) // default metrics are selected by default + ? 'on' + : undefined, 'data-test-subj': `${filterOptions.filterName}-filter-option`, })) : isDataStreamsFilter && !!filterOptions.options.length diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.test.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.test.tsx index c73e35fe1397d..20f091029f5b8 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.test.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.test.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import moment from 'moment'; -import { METRIC_TYPE_VALUES, MetricTypes } from '../../../common/rest_types'; +import { METRIC_TYPE_VALUES, type MetricTypes } from '../../../common/rest_types'; import { getDataUsageMetricsFiltersFromUrlParams } from './use_charts_url_params'; -// FLAKY: https://github.com/elastic/kibana/issues/200888 -describe.skip('#getDataUsageMetricsFiltersFromUrlParams', () => { +describe('#getDataUsageMetricsFiltersFromUrlParams', () => { const getMetricTypesAsArray = (): MetricTypes[] => { return [...METRIC_TYPE_VALUES]; }; @@ -58,12 +56,12 @@ describe.skip('#getDataUsageMetricsFiltersFromUrlParams', () => { it('should use given relative startDate and endDate values URL params', () => { expect( getDataUsageMetricsFiltersFromUrlParams({ - startDate: moment().subtract(24, 'hours').toISOString(), - endDate: moment().toISOString(), + startDate: 'now-9d', + endDate: 'now-24h/h', }) ).toEqual({ - endDate: moment().toISOString(), - startDate: moment().subtract(24, 'hours').toISOString(), + endDate: 'now-24h/h', + startDate: 'now-9d', }); }); diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx index ed833393ad7eb..3a1ba7dc1de62 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx @@ -6,7 +6,7 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { MetricTypes, isMetricType } from '../../../common/rest_types'; +import { type MetricTypes, isMetricType } from '../../../common/rest_types'; import { useUrlParams } from '../../hooks/use_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS } from './use_date_picker'; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx index 1b4b7e38e3554..f4d198461f733 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { useCallback, useState } from 'react'; import type { DurationRange, @@ -19,8 +18,10 @@ export const DEFAULT_DATE_RANGE_OPTIONS = Object.freeze({ enabled: false, duration: 10000, }, - startDate: moment().subtract(24, 'hours').startOf('day').toISOString(), - endDate: moment().toISOString(), + startDate: 'now-24h/h', + endDate: 'now', + maxDate: 'now+1s', + minDate: 'now-9d', recentlyUsedDateRanges: [], }); diff --git a/x-pack/plugins/data_usage/public/application.tsx b/x-pack/plugins/data_usage/public/application.tsx index 0e6cdc6192c7c..7bd2c794d5b3c 100644 --- a/x-pack/plugins/data_usage/public/application.tsx +++ b/x-pack/plugins/data_usage/public/application.tsx @@ -16,8 +16,8 @@ import { PerformanceContextProvider } from '@kbn/ebt-tools'; import { useKibanaContextForPluginProvider } from './utils/use_kibana'; import { DataUsageStartDependencies, DataUsagePublicStart } from './types'; import { PLUGIN_ID } from '../common'; -import { DataUsageMetricsPage } from './app/data_usage_metrics_page'; import { DataUsageReactQueryClientProvider } from '../common/query_client'; +import { DataUsageMetricsPage } from './app/data_usage_metrics_page'; export const renderApp = ( core: CoreStart, diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx index 04cee589a523d..5e224e635dca4 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx @@ -11,7 +11,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useGetDataUsageDataStreams } from './use_get_data_streams'; import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common'; import { coreMock as mockCore } from '@kbn/core/public/mocks'; -import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; +import { dataUsageTestQueryClientOptions } from '../../common/test_utils'; const useQueryMock = _useQuery as jest.Mock; diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx index 677bd4bdfcef1..1ddb84d89ffc9 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import moment from 'moment'; import React, { ReactNode } from 'react'; import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; import { useGetDataUsageMetrics } from './use_get_usage_metrics'; import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; import { coreMock as mockCore } from '@kbn/core/public/mocks'; -import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; +import { dataUsageTestQueryClientOptions, timeXMinutesAgo } from '../../common/test_utils'; const useQueryMock = _useQuery as jest.Mock; @@ -42,8 +41,8 @@ jest.mock('../utils/use_kibana', () => { }); const defaultUsageMetricsRequestBody = { - from: moment().subtract(15, 'minutes').toISOString(), - to: moment().toISOString(), + from: timeXMinutesAgo(15), + to: timeXMinutesAgo(0), metricTypes: ['ingest_rate'], dataStreams: ['ds-1'], }; diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 6b2ef5316b0f6..da5f3004d0024 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -8,8 +8,12 @@ import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { UsageMetricsRequestBody, UsageMetricsResponseSchemaBody } from '../../common/rest_types'; +import { dateParser } from '../../common/utils'; import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; +import type { + UsageMetricsRequestBody, + UsageMetricsResponseSchemaBody, +} from '../../common/rest_types'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; interface ErrorType { @@ -33,8 +37,8 @@ export const useGetDataUsageMetrics = ( signal, version: '1', body: JSON.stringify({ - from: body.from, - to: body.to, + from: dateParser(body.from), + to: dateParser(body.to), metricTypes: body.metricTypes, dataStreams: body.dataStreams, }), diff --git a/x-pack/plugins/data_usage/public/index.ts b/x-pack/plugins/data_usage/public/index.ts index e18b801a6a38f..3ac8c6950a045 100644 --- a/x-pack/plugins/data_usage/public/index.ts +++ b/x-pack/plugins/data_usage/public/index.ts @@ -11,7 +11,6 @@ import type { DataUsagePublicStart, DataUsageSetupDependencies, DataUsageStartDependencies, - ConfigSchema, } from './types'; import { DataUsagePlugin } from './plugin'; @@ -22,4 +21,5 @@ export const plugin: PluginInitializer< DataUsagePublicStart, DataUsageSetupDependencies, DataUsageStartDependencies -> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => new DataUsagePlugin(); +> = (pluginInitializerContext: PluginInitializerContext) => + new DataUsagePlugin(pluginInitializerContext); diff --git a/x-pack/plugins/data_usage/public/plugin.ts b/x-pack/plugins/data_usage/public/plugin.ts index aa3b02c2b671b..5878f85038829 100644 --- a/x-pack/plugins/data_usage/public/plugin.ts +++ b/x-pack/plugins/data_usage/public/plugin.ts @@ -5,16 +5,21 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { DataUsagePublicSetup, DataUsagePublicStart, DataUsageStartDependencies, DataUsageSetupDependencies, + DataUsagePublicConfigType, } from './types'; -import { PLUGIN_ID, PLUGIN_NAME } from '../common'; - +import { PLUGIN_ID } from '../common'; +import { PLUGIN_NAME } from './translations'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; export class DataUsagePlugin implements Plugin< @@ -24,30 +29,46 @@ export class DataUsagePlugin DataUsageStartDependencies > { + private config: DataUsagePublicConfigType; + private experimentalFeatures: ExperimentalFeatures; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get<DataUsagePublicConfigType>(); + this.experimentalFeatures = {} as ExperimentalFeatures; + } + public setup( core: CoreSetup<DataUsageStartDependencies, DataUsagePublicStart>, plugins: DataUsageSetupDependencies ): DataUsagePublicSetup { const { management } = plugins; - management.sections.section.data.registerApp({ - id: PLUGIN_ID, - title: PLUGIN_NAME, - order: 6, - keywords: ['data usage', 'usage'], - async mount(params: ManagementAppMountParams) { - const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([ - import('./application'), - core.getStartServices(), - ]); - - return renderApp(coreStart, pluginsStartDeps, pluginStart, params); - }, - }); + this.experimentalFeatures = parseExperimentalConfigValue( + this.config.enableExperimental + ).features; + + const experimentalFeatures = this.experimentalFeatures; + + if (!experimentalFeatures.dataUsageDisabled) { + management.sections.section.data.registerApp({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + order: 6, + keywords: ['data usage', 'usage'], + async mount(params: ManagementAppMountParams) { + const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([ + import('./application'), + core.getStartServices(), + ]); + + return renderApp(coreStart, pluginsStartDeps, pluginStart, params); + }, + }); + } return {}; } - public start(_core: CoreStart): DataUsagePublicStart { + public start(_core: CoreStart, plugins: DataUsageStartDependencies): DataUsagePublicStart { return {}; } diff --git a/x-pack/plugins/data_usage/public/app/translations.tsx b/x-pack/plugins/data_usage/public/translations.tsx similarity index 68% rename from x-pack/plugins/data_usage/public/app/translations.tsx rename to x-pack/plugins/data_usage/public/translations.tsx index ee42d3b58906b..0996ec2bb6d50 100644 --- a/x-pack/plugins/data_usage/public/app/translations.tsx +++ b/x-pack/plugins/data_usage/public/translations.tsx @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; +export const PLUGIN_NAME = i18n.translate('xpack.dataUsage.name', { + defaultMessage: 'Data Usage', +}); + export const FILTER_NAMES = Object.freeze({ metricTypes: i18n.translate('xpack.dataUsage.metrics.filter.metricTypes', { defaultMessage: 'Metric types', @@ -35,6 +39,9 @@ export const DATA_USAGE_PAGE = Object.freeze({ }); export const UX_LABELS = Object.freeze({ + filterSelectAll: i18n.translate('xpack.dataUsage.metrics.filter.selectAll', { + defaultMessage: 'Select all', + }), filterClearAll: i18n.translate('xpack.dataUsage.metrics.filter.clearAll', { defaultMessage: 'Clear all', }), @@ -48,4 +55,18 @@ export const UX_LABELS = Object.freeze({ defaultMessage: 'No {filterName} available', values: { filterName }, }), + dataQualityPopup: { + open: i18n.translate('xpack.dataUsage.metrics.dataQuality.open.actions', { + defaultMessage: 'Open data stream actions', + }), + copy: i18n.translate('xpack.dataUsage.metrics.dataQuality.copy.dataStream', { + defaultMessage: 'Copy data stream name', + }), + manage: i18n.translate('xpack.dataUsage.metrics.dataQuality.manage.dataStream', { + defaultMessage: 'Manage data stream', + }), + view: i18n.translate('xpack.dataUsage.metrics.dataQuality.view', { + defaultMessage: 'View data quality', + }), + }, }); diff --git a/x-pack/plugins/data_usage/public/types.ts b/x-pack/plugins/data_usage/public/types.ts index e65865dc31821..8c92d27c3d9b4 100644 --- a/x-pack/plugins/data_usage/public/types.ts +++ b/x-pack/plugins/data_usage/public/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { schema, TypeOf } from '@kbn/config-schema'; import { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; @@ -23,5 +24,21 @@ export interface DataUsageStartDependencies { management: ManagementStart; share: SharePluginStart; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ConfigSchema {} + +const schemaObject = schema.object({ + /** + * For internal use. A list of string values (comma delimited) that will enable experimental + * type of functionality that is not yet released. Valid values for this settings need to + * be defined in: + * `x-pack/plugins/dataUsage/common/experimental_features.ts` + * under the `allowedExperimentalValues` object + * + * @example + * xpack.dataUsage.enableExperimental: ['someFeature'] + */ + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + }), +}); + +export type DataUsagePublicConfigType = TypeOf<typeof schemaObject>; diff --git a/x-pack/plugins/data_usage/server/config.ts b/x-pack/plugins/data_usage/server/config.ts index c6721592b6aac..c00c08bb1b058 100644 --- a/x-pack/plugins/data_usage/server/config.ts +++ b/x-pack/plugins/data_usage/server/config.ts @@ -26,6 +26,19 @@ export const configSchema = schema.object({ ), }) ), + /** + * For internal use. A list of string values (comma delimited) that will enable experimental + * type of functionality that is not yet released. Valid values for this settings need to + * be defined in: + * `x-pack/plugins/dataUsage/common/experimental_features.ts` + * under the `allowedExperimentalValues` object + * + * @example + * xpack.dataUsage.enableExperimental: ['someFeature'] + */ + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + }), }); export type DataUsageConfigType = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/data_usage/server/index.ts b/x-pack/plugins/data_usage/server/index.ts index 66d839303d716..826486dc717f6 100644 --- a/x-pack/plugins/data_usage/server/index.ts +++ b/x-pack/plugins/data_usage/server/index.ts @@ -25,6 +25,9 @@ export type { DataUsageServerSetup, DataUsageServerStart }; export const config: PluginConfigDescriptor<DataUsageConfigType> = { schema: configSchema, + exposeToBrowser: { + enableExperimental: true, + }, }; export const plugin: PluginInitializer< diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts index 2330e465d9b12..374c4b9c82e7e 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts @@ -84,6 +84,48 @@ describe('registerDataStreamsRoute', () => { }); }); + it('should not include data streams with 0 size', async () => { + mockGetMeteringStats.mockResolvedValue({ + datastreams: [ + { + name: 'datastream1', + size_in_bytes: 100, + }, + { + name: 'datastream2', + size_in_bytes: 200, + }, + { + name: 'datastream3', + size_in_bytes: 0, + }, + { + name: 'datastream4', + size_in_bytes: 0, + }, + ], + }); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: [ + { + name: 'datastream2', + storageSizeBytes: 200, + }, + { + name: 'datastream1', + storageSizeBytes: 100, + }, + ], + }); + }); + it('should return correct error if metering stats request fails', async () => { // using custom error for test here to avoid having to import the actual error class mockGetMeteringStats.mockRejectedValue( @@ -105,7 +147,7 @@ describe('registerDataStreamsRoute', () => { it.each([ ['no datastreams', {}, []], ['empty array', { datastreams: [] }, []], - ['an empty element', { datastreams: [{}] }, [{ name: undefined, storageSizeBytes: 0 }]], + ['an empty element', { datastreams: [{}] }, []], ])('should return empty array when no stats data with %s', async (_, stats, res) => { mockGetMeteringStats.mockResolvedValue(stats); const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts index 9abd898358e9e..99b4e982c5a40 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts @@ -27,10 +27,15 @@ export const getDataStreamsHandler = ( meteringStats && !!meteringStats.length ? meteringStats .sort((a, b) => b.size_in_bytes - a.size_in_bytes) - .map((stat) => ({ - name: stat.name, - storageSizeBytes: stat.size_in_bytes ?? 0, - })) + .reduce<Array<{ name: string; storageSizeBytes: number }>>((acc, stat) => { + if (stat.size_in_bytes > 0) { + acc.push({ + name: stat.name, + storageSizeBytes: stat.size_in_bytes ?? 0, + }); + } + return acc; + }, []) : []; return response.ok({ diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts index d6337bbcc8dcd..c0eb0e5e8ef2d 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import moment from 'moment'; import type { MockedKeys } from '@kbn/utility-types-jest'; import type { CoreSetup } from '@kbn/core/server'; import { registerUsageMetricsRoute } from './usage_metrics'; @@ -20,6 +19,7 @@ import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common'; import { createMockedDataUsageContext } from '../../mocks'; import { CustomHttpRequestError } from '../../utils'; import { AutoOpsError } from '../../services/errors'; +import { timeXMinutesAgo } from '../../../common/test_utils'; describe('registerUsageMetricsRoute', () => { let mockCore: MockedKeys<CoreSetup<{}, DataUsageServerStart>>; @@ -56,8 +56,8 @@ describe('registerUsageMetricsRoute', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - from: moment().subtract(15, 'minutes').toISOString(), - to: moment().toISOString(), + from: timeXMinutesAgo(15), + to: timeXMinutesAgo(0), metricTypes: ['ingest_rate'], dataStreams: [], }, @@ -123,8 +123,8 @@ describe('registerUsageMetricsRoute', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - from: moment().subtract(15, 'minutes').toISOString(), - to: moment().toISOString(), + from: timeXMinutesAgo(15), + to: timeXMinutesAgo(0), metricTypes: ['ingest_rate', 'storage_retained'], dataStreams: ['.ds-1', '.ds-2'], }, @@ -191,8 +191,8 @@ describe('registerUsageMetricsRoute', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - from: moment().subtract(15, 'minutes').toISOString(), - to: moment().toISOString(), + from: timeXMinutesAgo(15), + to: timeXMinutesAgo(0), metricTypes: ['ingest_rate'], dataStreams: ['.ds-1', '.ds-2'], }, diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts index 6907a683696a7..c2dee4ca2ce52 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { chunk } from 'lodash/fp'; import { RequestHandler } from '@kbn/core/server'; -import { +import type { MetricTypes, UsageMetricsAutoOpsResponseSchemaBody, UsageMetricsRequestBody, @@ -30,6 +31,8 @@ export const getUsageMetricsHandler = ( const core = await context.core; const esClient = core.elasticsearch.client.asCurrentUser; + const getDataStreams = (name: string[]) => + esClient.indices.getDataStream({ name, expand_wildcards: 'all' }); logger.debug(`Retrieving usage metrics`); const { from, to, metricTypes, dataStreams: requestDsNames } = request.body; @@ -43,15 +46,24 @@ export const getUsageMetricsHandler = ( new CustomHttpRequestError('[request body.dataStreams]: no data streams selected', 400) ); } - let dataStreamsResponse; + + let dataStreamsResponse: Array<{ name: string }>; try { - // Attempt to fetch data streams - const { data_streams: dataStreams } = await esClient.indices.getDataStream({ - name: requestDsNames, - expand_wildcards: 'all', - }); - dataStreamsResponse = dataStreams; + if (requestDsNames.length <= 50) { + logger.debug(`Retrieving usage metrics`); + const { data_streams: dataStreams } = await getDataStreams(requestDsNames); + dataStreamsResponse = dataStreams; + } else { + logger.debug(`Retrieving usage metrics in chunks of 50`); + // Attempt to fetch data streams in chunks of 50 + const dataStreamsChunks = Math.ceil(requestDsNames.length / 50); + const chunkedDsLists = chunk(dataStreamsChunks, requestDsNames); + const chunkedDataStreams = await Promise.all( + chunkedDsLists.map((dsList) => getDataStreams(dsList)) + ); + dataStreamsResponse = chunkedDataStreams.flatMap((ds) => ds.data_streams); + } } catch (error) { return errorHandler( logger, diff --git a/x-pack/plugins/data_usage/server/services/autoops_api.ts b/x-pack/plugins/data_usage/server/services/autoops_api.ts index 9fd742a3e73fa..2ff824e04f6dd 100644 --- a/x-pack/plugins/data_usage/server/services/autoops_api.ts +++ b/x-pack/plugins/data_usage/server/services/autoops_api.ts @@ -6,7 +6,7 @@ */ import https from 'https'; -import dateMath from '@kbn/datemath'; + import { SslConfig, sslSchema } from '@kbn/server-http-tools'; import apm from 'elastic-apm-node'; @@ -16,9 +16,10 @@ import axios from 'axios'; import { LogMeta } from '@kbn/core/server'; import { UsageMetricsAutoOpsResponseSchema, - UsageMetricsAutoOpsResponseSchemaBody, - UsageMetricsRequestBody, + type UsageMetricsAutoOpsResponseSchemaBody, + type UsageMetricsRequestBody, } from '../../common/rest_types'; +import { dateParser } from '../../common/utils'; import { AutoOpsConfig } from '../types'; import { AutoOpsError } from './errors'; import { appContextService } from './app_context'; @@ -30,7 +31,6 @@ const AUTO_OPS_MISSING_CONFIG_ERROR = 'Missing autoops configuration'; const getAutoOpsAPIRequestUrl = (url?: string, projectId?: string): string => `${url}/monitoring/serverless/v1/projects/${projectId}/metrics`; -const dateParser = (date: string) => dateMath.parse(date)?.toISOString(); export class AutoOpsAPIService { private logger: Logger; constructor(logger: Logger) { diff --git a/x-pack/plugins/data_usage/server/services/index.ts b/x-pack/plugins/data_usage/server/services/index.ts index 69db6b590c6f3..56e449c8a5679 100644 --- a/x-pack/plugins/data_usage/server/services/index.ts +++ b/x-pack/plugins/data_usage/server/services/index.ts @@ -6,7 +6,7 @@ */ import { ValidationError } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; -import { MetricTypes } from '../../common/rest_types'; +import type { MetricTypes } from '../../common/rest_types'; import { AutoOpsError } from './errors'; import { AutoOpsAPIService } from './autoops_api'; diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index 309bad3e1b63c..8647f7957451a 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -33,6 +33,8 @@ "@kbn/server-http-tools", "@kbn/utility-types-jest", "@kbn/datemath", + "@kbn/ui-theme", + "@kbn/i18n-react", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index ada69e95f32a1..6839f4c2c18e1 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "target/types", }, - "include": ["*.ts", "common/**/*", "public/**/*", "server/**/*"], + "include": ["*.ts", "common/**/*", "public/**/*", "server/**/*", "ui_tests/**/*"], "kbn_references": [ "@kbn/core", "@kbn/data-plugin", @@ -21,6 +21,7 @@ "@kbn/presentation-publishing", "@kbn/data-views-plugin", "@kbn/unified-search-plugin", + "@kbn/scout", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/discover_enhanced/ui_tests/README.md b/x-pack/plugins/discover_enhanced/ui_tests/README.md new file mode 100644 index 0000000000000..8320e9464d9ca --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/README.md @@ -0,0 +1,17 @@ +## How to run tests +First start the servers with + +```bash +// ESS +node scripts/scout_start_servers.js --stateful +// Serverless +node scripts/scout_start_servers.js --serverless=es +``` + +Then you can run the tests multiple times in another terminal with: + +```bash +npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts +``` + +Test results are available in `x-pack/plugins/discover_enhanced/ui_tests/output` diff --git a/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts new file mode 100644 index 0000000000000..38d4905f82e6f --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + test as base, + PageObjects, + createLazyPageObject, + ScoutTestFixtures, + ScoutWorkerFixtures, +} from '@kbn/scout'; +import { DemoPage } from './page_objects'; + +interface ExtendedScoutTestFixtures extends ScoutTestFixtures { + pageObjects: PageObjects & { + demo: DemoPage; + }; +} + +export const test = base.extend<ExtendedScoutTestFixtures, ScoutWorkerFixtures>({ + pageObjects: async ({ pageObjects, page }, use) => { + const extendedPageObjects = { + ...pageObjects, + demo: createLazyPageObject(DemoPage, page), + }; + + await use(extendedPageObjects); + }, +}); diff --git a/x-pack/plugins/entity_manager/server/lib/errors.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/demo.ts similarity index 51% rename from x-pack/plugins/entity_manager/server/lib/errors.ts rename to x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/demo.ts index e0d341f87a9fd..4c65384b9c816 100644 --- a/x-pack/plugins/entity_manager/server/lib/errors.ts +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/demo.ts @@ -5,10 +5,12 @@ * 2.0. */ -export class AssetNotFoundError extends Error { - constructor(ean: string) { - super(`Asset with ean (${ean}) not found in the provided time range`); - Object.setPrototypeOf(this, new.target.prototype); - this.name = 'AssetNotFoundError'; +import { ScoutPage } from '@kbn/scout'; + +export class DemoPage { + constructor(private readonly page: ScoutPage) {} + + async goto() { + this.page.gotoApp('not_implemented'); } } diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/index.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/index.ts similarity index 82% rename from x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/index.ts rename to x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/index.ts index dcb2d12f6c986..47afc9c11fe7b 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/index.ts +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/page_objects/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { AddEmptyPrompt } from './add_empty_prompt'; +export { DemoPage } from './demo'; diff --git a/x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts b/x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts new file mode 100644 index 0000000000000..34b370396b67e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createPlaywrightConfig } from '@kbn/scout'; + +// eslint-disable-next-line import/no-default-export +export default createPlaywrightConfig({ + testDir: './tests', +}); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts new file mode 100644 index 0000000000000..ff1389e85924e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.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 { expect } from '@kbn/scout'; +import { test } from '../fixtures'; + +test.describe('Discover app - value suggestions', () => { + test.beforeAll(async ({ esArchiver, kbnClient }) => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kbnClient.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/dashboard_drilldowns/drilldowns' + ); + await kbnClient.uiSettings.update({ + defaultIndex: 'logstash-*', // TODO: investigate why it is required for `node scripts/playwright_test.js` run + 'doc_table:legacy': false, + }); + }); + + test.afterAll(async ({ kbnClient }) => { + await kbnClient.uiSettings.unset('doc_table:legacy'); + await kbnClient.uiSettings.unset('defaultIndex'); + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.discover.goto(); + }); + + test('dont show up if outside of range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange({ + from: 'Mar 1, 2020 @ 00:00:00.000', + to: 'Nov 1, 2020 @ 00:00:00.000', + }); + + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(0); + }); + + test('show up if in range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange({ + from: 'Sep 19, 2015 @ 06:31:44.000', + to: 'Sep 23, 2015 @ 18:31:44.000', + }); + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(5); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('jpg'); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts new file mode 100644 index 0000000000000..4ba9450869313 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.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 { expect } from '@kbn/scout'; +import { test } from '../fixtures'; + +test.describe('Discover app - value suggestions non-time based', () => { + test.beforeAll(async ({ esArchiver, kbnClient }) => { + await esArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' + ); + await kbnClient.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + await kbnClient.uiSettings.update({ + defaultIndex: 'without-timefield', + 'doc_table:legacy': false, + }); + }); + + test.afterAll(async ({ kbnClient }) => { + await kbnClient.uiSettings.unset('doc_table:legacy'); + await kbnClient.uiSettings.unset('defaultIndex'); + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.discover.goto(); + }); + + test('shows all auto-suggest options for a filter in discover context app', async ({ page }) => { + await page.testSubj.fill('queryInput', 'type.keyword : '); + await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(1); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('"apache"'); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index 231aa1c319da4..c3ce7fb1a43a0 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -178,9 +178,22 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { public createInferenceEndpoint = async () => { const elserId = await this.options.getElserId(); this.options.logger.debug(`Deploying ELSER model '${elserId}'...`); + const esClient = await this.options.elasticsearchClientPromise; + try { - const esClient = await this.options.elasticsearchClientPromise; + await esClient.inference.delete({ + inference_id: ASSISTANT_ELSER_INFERENCE_ID, + // it's being used in the mapping so we need to force delete + force: true, + }); + this.options.logger.debug(`Deleted existing inference endpoint for ELSER model '${elserId}'`); + } catch (error) { + this.options.logger.error( + `Error deleting inference endpoint for ELSER model '${elserId}':\n${error}` + ); + } + try { await esClient.inference.put({ task_type: 'sparse_embedding', inference_id: ASSISTANT_ELSER_INFERENCE_ID, @@ -198,6 +211,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { task_settings: {}, }, }); + + // await for the model to be deployed + await this.isInferenceEndpointExists(); } catch (error) { this.options.logger.error( `Error creating inference endpoint for ELSER model '${elserId}':\n${error}` diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 0c13a772862b8..2603ea3d89018 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -211,7 +211,7 @@ export const SEARCH_RELEVANCE_PLUGIN = { DESCRIPTION: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.description', { defaultMessage: 'Manage your inference endpoints for semantic search and AI use cases.', }), - URL: '/app/enterprise_search/relevance', + URL: '/app/elasticsearch/relevance', LOGO: 'logoEnterpriseSearch', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/', }; diff --git a/x-pack/plugins/enterprise_search/common/locators/index.ts b/x-pack/plugins/enterprise_search/common/locators/index.ts index 0f9d2060cd829..35c1d43b3b30a 100644 --- a/x-pack/plugins/enterprise_search/common/locators/index.ts +++ b/x-pack/plugins/enterprise_search/common/locators/index.ts @@ -6,14 +6,17 @@ */ import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import { SerializableRecord } from '@kbn/utility-types'; import { CreateIndexLocatorDefinition, type CreateIndexLocatorParams, } from './create_index_locator'; +import { SearchInferenceEndpointLocatorDefinition } from './inference_locator'; import { PlaygroundLocatorDefinition, type PlaygroundLocatorParams } from './playground_locator'; export function registerLocators(share: SharePluginSetup) { share.url.locators.create<CreateIndexLocatorParams>(new CreateIndexLocatorDefinition()); share.url.locators.create<PlaygroundLocatorParams>(new PlaygroundLocatorDefinition()); + share.url.locators.create<SerializableRecord>(new SearchInferenceEndpointLocatorDefinition()); } diff --git a/x-pack/plugins/enterprise_search/common/locators/inference_locator.tsx b/x-pack/plugins/enterprise_search/common/locators/inference_locator.tsx new file mode 100644 index 0000000000000..f20d628bf1899 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/locators/inference_locator.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition } from '@kbn/share-plugin/common'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; + +import { SEARCH_RELEVANCE_PLUGIN } from '../constants'; + +export function registerLocators(share: SharePluginSetup) { + share.url.locators.create<SerializableRecord>(new SearchInferenceEndpointLocatorDefinition()); +} + +export class SearchInferenceEndpointLocatorDefinition + implements LocatorDefinition<SerializableRecord> +{ + public readonly getLocation = async () => { + return { + app: SEARCH_RELEVANCE_PLUGIN.ID, + path: '/inference_endpoints', + state: {}, + }; + }; + + public readonly id = 'SEARCH_INFERENCE_ENDPOINTS'; +} diff --git a/x-pack/plugins/enterprise_search/kibana.jsonc b/x-pack/plugins/enterprise_search/kibana.jsonc index e284ae1862144..65343904ba7fc 100644 --- a/x-pack/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/plugins/enterprise_search/kibana.jsonc @@ -20,7 +20,7 @@ "logsShared", "logsDataAccess", "esUiShared", - "navigation" + "navigation", ], "optionalPlugins": [ "customIntegrations", @@ -34,8 +34,9 @@ "guidedOnboarding", "console", "searchConnectors", - "searchPlayground", "searchInferenceEndpoints", + "searchNavigation", + "searchPlayground", "embeddable", "discover", "charts", diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_create_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_create_button.test.tsx new file mode 100644 index 0000000000000..548314dcda43b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_create_button.test.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 React, { type ReactNode } from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { waitForEuiToolTipVisible } from '@elastic/eui/lib/test/rtl'; + +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +import { CreateSearchApplicationButton } from './search_applications_list'; + +function Container({ children }: { children?: ReactNode }) { + return <IntlProvider locale="en">{children}</IntlProvider>; +} + +describe('CreateSearchApplicationButton', () => { + test('disabled={false}', async () => { + render( + <Container> + <CreateSearchApplicationButton disabled={false} /> + </Container> + ); + + await userEvent.hover( + await screen.findByTestId('enterprise-search-search-applications-creation-button') + ); + + await waitForEuiToolTipVisible(); + + expect( + await screen.findByTestId('create-search-application-button-popover-content') + ).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.test.tsx index f34aac9e4359c..2683a0afc0caa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.test.tsx @@ -9,7 +9,7 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; +import { shallowWithIntl } from '@kbn/test-jest-helpers'; import { Status } from '../../../../../common/types/api'; @@ -116,80 +116,3 @@ describe('SearchApplicationsList', () => { expect(wrapper.find(LicensingCallout)).toHaveLength(0); }); }); - -describe('CreateSearchApplicationButton', () => { - describe('disabled={true}', () => { - it('renders a disabled button that shows a popover when hovered', () => { - const wrapper = mountWithIntl(<CreateSearchApplicationButton disabled />); - - const button = wrapper.find( - 'button[data-test-subj="enterprise-search-search-applications-creation-button"]' - ); - - expect(button).toHaveLength(1); - expect(button.prop('disabled')).toBeTruthy(); - - let popover = wrapper.find( - 'div[data-test-subj="create-search-application-button-popover-content"]' - ); - - expect(popover).toHaveLength(0); - - const hoverTarget = wrapper.find( - 'div[data-test-subj="create-search-application-button-hover-target"]' - ); - - expect(hoverTarget).toHaveLength(1); - - hoverTarget.simulate('mouseEnter'); - - wrapper.update(); - - popover = wrapper.find( - 'div[data-test-subj="create-search-application-button-popover-content"]' - ); - - expect(popover).toHaveLength(1); - expect(popover.text()).toMatch( - 'This functionality may be changed or removed completely in a future release.' - ); - }); - }); - describe('disabled={false}', () => { - it('renders a button and shows a popover when hovered', () => { - const wrapper = mountWithIntl(<CreateSearchApplicationButton disabled={false} />); - - const button = wrapper.find( - 'button[data-test-subj="enterprise-search-search-applications-creation-button"]' - ); - - expect(button).toHaveLength(1); - expect(button.prop('disabled')).toBeFalsy(); - - let popover = wrapper.find( - 'div[data-test-subj="create-search-application-button-popover-content"]' - ); - - expect(popover).toHaveLength(0); - - const hoverTarget = wrapper.find( - 'div[data-test-subj="create-search-application-button-hover-target"]' - ); - - expect(hoverTarget).toHaveLength(1); - - hoverTarget.simulate('mouseEnter'); - - wrapper.update(); - - popover = wrapper.find( - 'div[data-test-subj="create-search-application-button-popover-content"]' - ); - - expect(popover).toHaveLength(1); - expect(popover.text()).toMatch( - 'This functionality may be changed or removed completely in a future release.' - ); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.tsx index 126c44b6b5dca..fbc07eef5c5e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_applications/search_applications_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import useThrottle from 'react-use/lib/useThrottle'; @@ -17,10 +17,9 @@ import { EuiFlexItem, EuiIcon, EuiLink, - EuiPopover, - EuiPopoverTitle, EuiSpacer, EuiText, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -53,38 +52,10 @@ interface CreateSearchApplicationButtonProps { export const CreateSearchApplicationButton: React.FC<CreateSearchApplicationButtonProps> = ({ disabled, }) => { - const [showPopover, setShowPopover] = useState<boolean>(false); - return ( - <EuiPopover - isOpen={showPopover} - closePopover={() => setShowPopover(false)} - button={ - <div - data-test-subj="create-search-application-button-hover-target" - onMouseEnter={() => setShowPopover(true)} - onMouseLeave={() => setShowPopover(false)} - tabIndex={0} - > - <EuiButton - fill - iconType="plusInCircle" - data-test-subj="enterprise-search-search-applications-creation-button" - data-telemetry-id="entSearchApplications-list-createSearchApplication" - isDisabled={disabled} - onClick={() => KibanaLogic.values.navigateToUrl(SEARCH_APPLICATION_CREATION_PATH)} - > - {i18n.translate( - 'xpack.enterpriseSearch.searchApplications.list.createSearchApplicationButton.label', - { - defaultMessage: 'Create', - } - )} - </EuiButton> - </div> - } - > - <EuiPopoverTitle> + <EuiToolTip + position="top" + title={ <EuiFlexGroup justifyContent="center" gutterSize="s"> <EuiFlexItem grow={false}> <EuiIcon type="beaker" /> @@ -96,23 +67,35 @@ export const CreateSearchApplicationButton: React.FC<CreateSearchApplicationButt /> </EuiFlexItem> </EuiFlexGroup> - </EuiPopoverTitle> - <div - style={{ width: '300px' }} - data-test-subj="create-search-application-button-popover-content" + } + content={ + <EuiText size="s" data-test-subj="create-search-application-button-popover-content"> + <FormattedMessage + id="xpack.enterpriseSearch.searchApplications.list.createSearchApplicationTechnicalPreviewPopover.body" + defaultMessage="This functionality may be changed or removed completely in a future release." + /> + </EuiText> + } + > + <EuiButton + fill + iconType="plusInCircle" + data-test-subj="enterprise-search-search-applications-creation-button" + data-telemetry-id="entSearchApplications-list-createSearchApplication" + isDisabled={disabled} + onClick={() => KibanaLogic.values.navigateToUrl(SEARCH_APPLICATION_CREATION_PATH)} > - <EuiFlexGroup direction="column" gutterSize="m"> - <EuiText size="s"> - <FormattedMessage - id="xpack.enterpriseSearch.searchApplications.list.createSearchApplicationTechnicalPreviewPopover.body" - defaultMessage="This functionality may be changed or removed completely in a future release." - /> - </EuiText> - </EuiFlexGroup> - </div> - </EuiPopover> + {i18n.translate( + 'xpack.enterpriseSearch.searchApplications.list.createSearchApplicationButton.label', + { + defaultMessage: 'Create', + } + )} + </EuiButton> + </EuiToolTip> ); }; + interface ListProps { createSearchApplicationFlyoutOpen?: boolean; } @@ -223,6 +206,7 @@ export const SearchApplicationsList: React.FC<ListProps> = ({ <> <div> <EuiFieldSearch + data-test-subj="enterpriseSearchSearchApplicationsListFieldSearch" value={searchQuery} placeholder={i18n.translate( 'xpack.enterpriseSearch.searchApplications.list.searchBar.placeholder', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/attach_index_box.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/attach_index_box.tsx index 8b5bb17020ac0..5a2e279026bda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/attach_index_box.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/attach_index_box.tsx @@ -222,6 +222,12 @@ export const AttachIndexBox: React.FC<AttachIndexBoxProps> = ({ connector }) => )} isLoading={isLoading} options={groupedOptions} + onKeyDown={(event) => { + // Index name should not contain spaces + if (event.key === ' ') { + event.preventDefault(); + } + }} onSearchChange={(searchValue) => { setQuery({ isFullMatch: options.some((option) => option.label === searchValue), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/docker_instructions_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/docker_instructions_step.tsx index ea5b59c4d3263..9202e5af7d9f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/docker_instructions_step.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/docker_instructions_step.tsx @@ -58,7 +58,10 @@ export const DockerInstructionsStep: React.FC<DockerInstructionsStepProps> = ({ host: elasticsearchUrl, }); - const escapedConfigYamlContent = configYamlContent.replace(/"/g, '\\"').replace(/\$/g, '\\$'); + const escapedConfigYamlContent = configYamlContent + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\$/g, '\\$'); const createConfigCommand = `mkdir -p "$HOME/elastic-connectors" && echo "${escapedConfigYamlContent}" > "$HOME/elastic-connectors/config.yml"`; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_description.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_description.tsx new file mode 100644 index 0000000000000..c6f41acb10da8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_description.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Connector } from '@kbn/search-connectors'; + +import { ConnectorField } from './connector_field'; + +export const ConnectorDescription: React.FC<{ connector: Connector }> = ({ connector }) => ( + <ConnectorField connector={connector} field="description" /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx index 4c787e9ef28ef..b3251ad5a15b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx @@ -28,7 +28,8 @@ import { SearchIndexPipelines } from '../search_index/pipelines/pipelines'; import { getHeaderActions } from '../shared/header_actions/header_actions'; import { ConnectorConfiguration } from './connector_configuration'; -import { ConnectorNameAndDescription } from './connector_name_and_description'; +import { ConnectorDescription } from './connector_description'; +import { ConnectorName } from './connector_name'; import { ConnectorViewLogic } from './connector_view_logic'; import { ConnectorDetailOverview } from './overview'; @@ -246,7 +247,8 @@ export const ConnectorDetail: React.FC = () => { pageViewTelemetry={tabId} isLoading={isLoading} pageHeader={{ - pageTitle: connector ? <ConnectorNameAndDescription connector={connector} /> : '...', + description: connector ? <ConnectorDescription connector={connector} /> : '...', + pageTitle: connector ? <ConnectorName connector={connector} /> : '...', rightSideGroupProps: { gutterSize: 's', responsive: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_field.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_field.tsx new file mode 100644 index 0000000000000..8bddef94243ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_field.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ChangeEvent, useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiFlexItem, EuiInlineEditText, EuiInlineEditTitle } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { Connector } from '@kbn/search-connectors'; + +import { ConnectorNameAndDescriptionLogic } from './connector_name_and_description_logic'; + +interface ConnectorFieldProps { + connector: Connector; + field: 'name' | 'description'; // The field to edit + isTitle?: boolean; // Whether to render a title (`EuiInlineEditTitle`) or text (`EuiInlineEditText`) +} + +export const ConnectorField: React.FC<ConnectorFieldProps> = ({ connector, field, isTitle }) => { + const [value, setValue] = useState<string | null>(connector[field]); + const [resolverObject, setResolverObject] = useState({ + rej: () => {}, + res: () => {}, + }); + const { saveNameAndDescription, setConnector } = useActions(ConnectorNameAndDescriptionLogic); + const { status, isLoading, isFailed, isSuccess } = useValues(ConnectorNameAndDescriptionLogic); + + useEffect(() => { + setConnector(connector); + }, [connector]); + + useEffect(() => { + if (isSuccess) resolverObject.res(); + if (isFailed) resolverObject.rej(); + }, [status]); + + const getValidationPromiseResolvers = () => { + const resolvers = { + rej: () => {}, + res: () => {}, + }; + const promise = new Promise<void>((resolve, reject) => { + resolvers.res = resolve; + resolvers.rej = reject; + }); + setResolverObject(resolvers); + return promise; + }; + + const handleSave = async (newValue: string) => { + setValue(newValue); + saveNameAndDescription({ ...connector, [field]: newValue }); + await getValidationPromiseResolvers(); + return true; + }; + + const handleCancel = (previousValue: string) => { + setValue(previousValue); + }; + + const Component = isTitle ? EuiInlineEditTitle : EuiInlineEditText; + + return ( + <EuiFlexItem grow={false}> + <Component + inputAriaLabel={i18n.translate( + `xpack.enterpriseSearch.content.connectors.nameAndDescription.${field}.ariaLabel`, + { + defaultMessage: `Edit connector ${field}`, + } + )} + placeholder={i18n.translate( + `xpack.enterpriseSearch.content.connectors.nameAndDescription.${field}.placeholder`, + { + defaultMessage: field === 'name' ? 'Add a name to your connector' : 'Add a description', + } + )} + value={value || ''} + isLoading={isLoading} + isInvalid={field === 'name' && !value?.trim()} + size="m" + heading={isTitle ? 'h1' : 'span'} + editModeProps={{ + cancelButtonProps: { onClick: () => handleCancel(connector[field] || '') }, + formRowProps: + field === 'name' && !value?.trim() + ? { + error: [ + i18n.translate( + 'xpack.enterpriseSearch.content.nameAndDescription.name.error.empty', + { defaultMessage: 'Connector name cannot be empty' } + ), + ], + } + : undefined, + inputProps: { readOnly: isLoading }, + }} + onSave={handleSave} + onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)} + onCancel={() => handleCancel(connector[field] || '')} + /> + </EuiFlexItem> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name.tsx new file mode 100644 index 0000000000000..54f31c4beb6e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Connector } from '@kbn/search-connectors'; + +import { ConnectorField } from './connector_field'; + +export const ConnectorName: React.FC<{ connector: Connector }> = ({ connector }) => ( + <ConnectorField connector={connector} field="name" isTitle /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name_and_description.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name_and_description.tsx deleted file mode 100644 index cab9624a2d197..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_name_and_description.tsx +++ /dev/null @@ -1,142 +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, { ChangeEvent, useEffect, useState } from 'react'; - -import { useActions, useValues } from 'kea'; - -import { EuiFlexGroup, EuiFlexItem, EuiInlineEditText, EuiInlineEditTitle } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { Connector } from '@kbn/search-connectors'; - -import { ConnectorNameAndDescriptionLogic } from './connector_name_and_description_logic'; - -export interface ConnectorNameAndDescriptionProps { - connector: Connector; -} - -export interface ResolverObject { - rej: (value: boolean | void | PromiseLike<boolean | void>) => void; - res: (value: boolean | void | PromiseLike<boolean | void>) => void; -} - -let promise: Promise<boolean | void> | undefined; - -const getValidationPromiseResolvers = (): ResolverObject => { - const resolvers = { - rej: () => {}, - res: () => {}, - }; - promise = new Promise((resolve, reject) => { - resolvers.res = resolve; - resolvers.rej = reject; - }); - return resolvers; -}; - -export const ConnectorNameAndDescription: React.FC<ConnectorNameAndDescriptionProps> = ({ - connector, -}) => { - const [resolverObject, setResolverObject] = useState<ResolverObject>({ - rej: () => {}, - res: () => {}, - }); - const [connectorName, setConnectorName] = useState<string>(connector.name); - const [connectorDescription, setConnectorDescription] = useState<string | null>( - connector.description - ); - const [nameErrors, setNameErrors] = useState<string[]>([]); - const { saveNameAndDescription, setConnector } = useActions(ConnectorNameAndDescriptionLogic); - const { status, isLoading, isFailed, isSuccess } = useValues(ConnectorNameAndDescriptionLogic); - useEffect(() => { - setConnector(connector); - }, [connector]); - - useEffect(() => { - if (isSuccess) { - resolverObject.res(true); - } - if (isFailed) { - resolverObject.rej(); - } - }, [status]); - - return ( - <EuiFlexGroup direction="column" gutterSize="xs"> - <EuiFlexItem grow={false}> - <EuiInlineEditTitle - heading="h1" - inputAriaLabel={i18n.translate( - 'xpack.enterpriseSearch.content.connectors.nameAndDescription.name.ariaLabel', - { defaultMessage: 'Edit connector name' } - )} - placeholder={i18n.translate( - 'xpack.enterpriseSearch.content.connectors.nameAndDescription.name.placeholder', - { defaultMessage: 'Add a name to your connector' } - )} - value={connectorName} - isLoading={isLoading} - isInvalid={nameErrors.length > 0} - size="m" - editModeProps={{ - formRowProps: { error: nameErrors }, - cancelButtonProps: { onClick: () => setNameErrors([]) }, - inputProps: { readOnly: isLoading }, - }} - onSave={(inputValue) => { - if (inputValue.trim().length <= 0) { - setNameErrors([ - i18n.translate( - 'xpack.enterpriseSearch.content.nameAndDescription.name.error.empty', - { defaultMessage: 'Connector name cannot be empty' } - ), - ]); - return false; - } - setConnectorName(inputValue); - saveNameAndDescription({ description: connectorDescription, name: inputValue }); - setResolverObject(getValidationPromiseResolvers()); - return promise; - }} - onChange={(event: ChangeEvent<HTMLInputElement>) => { - setConnectorName(event.target.value); - }} - onCancel={(previousValue) => { - setConnectorName(previousValue); - }} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiInlineEditText - isLoading={isLoading} - inputAriaLabel={i18n.translate( - 'xpack.enterpriseSearch.content.connectors.nameAndDescription.description.ariaLabel', - { defaultMessage: 'Edit connector description' } - )} - placeholder={i18n.translate( - 'xpack.enterpriseSearch.content.connectors.nameAndDescription.description.placeholder', - { defaultMessage: 'Add a description' } - )} - value={connectorDescription || ''} - onSave={(inputValue) => { - setConnectorDescription(inputValue); - saveNameAndDescription({ description: inputValue, name: connectorName }); - setResolverObject(getValidationPromiseResolvers()); - return promise; - }} - onChange={(event: ChangeEvent<HTMLInputElement>) => { - setConnectorDescription(event.target.value); - }} - onCancel={(previousValue) => { - setConnectorDescription(previousValue); - }} - /> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx index 82f258487f39e..4f7937a0f1c76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx @@ -16,6 +16,7 @@ import { EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -221,7 +222,18 @@ export const ConnectorsTable: React.FC<ConnectorsTableProps> = ({ columns={columns} onChange={onChange} tableLayout="fixed" + tableCaption={i18n.translate( + 'xpack.enterpriseSearch.connectorsTable.table.availableConnectorsTableCaption', + { defaultMessage: 'Available connectors table' } + )} loading={isLoading} + noItemsMessage={ + <EuiText aria-live="polite" size="s"> + {i18n.translate('xpack.enterpriseSearch.connectorsTable.table.noResultsMessage', { + defaultMessage: 'No connectors found', + })} + </EuiText> + } pagination={{ pageIndex: meta.page.from / (meta.page.size || 1), pageSize: meta.page.size, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector.tsx similarity index 54% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector.tsx index 7019fcbb71e3f..21fa8e3f89f6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/react'; import { useActions, useValues } from 'kea'; import { @@ -18,11 +19,18 @@ import { EuiFlexGroup, EuiText, useEuiTheme, + EuiTextTruncate, + EuiBadgeGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import connectorLogo from '../../../../../../assets/images/connector.svg'; +import { + BETA_LABEL, + TECH_PREVIEW_LABEL, + CONNECTOR_CLIENT_LABEL, +} from '../../../../../shared/constants'; import { KibanaLogic } from '../../../../../shared/kibana'; import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic'; import { SelfManagePreference } from '../create_connector'; @@ -34,9 +42,7 @@ interface OptionData { secondaryContent?: string; } -export const ChooseConnectorSelectable: React.FC<ChooseConnectorSelectableProps> = ({ - selfManaged, -}) => { +export const ChooseConnector: React.FC<ChooseConnectorSelectableProps> = ({ selfManaged }) => { const { euiTheme } = useEuiTheme(); const [selectedOption, setSelectedOption] = useState<Array<EuiComboBoxOptionOption<OptionData>>>( [] @@ -52,20 +58,26 @@ export const ChooseConnectorSelectable: React.FC<ChooseConnectorSelectableProps> }; return ( <EuiFlexGroup - gutterSize="m" - key={key + '-span'} - justifyContent="spaceBetween" className={contentClassName} + key={key + '-span'} + gutterSize="m" + responsive={false} + direction="row" > - <EuiFlexGroup gutterSize="m"> - <EuiFlexItem grow={false}>{_prepend}</EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText size="s" textAlign="left"> - {label} - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexItem grow={false}>{_append}</EuiFlexItem> + <EuiFlexItem grow={false}>{_prepend}</EuiFlexItem> + <EuiFlexItem + css={css` + overflow: auto; + `} + grow + > + <EuiText textAlign="left" size="s"> + <EuiTextTruncate text={label} truncation="end" /> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiBadgeGroup gutterSize="xs">{_append}</EuiBadgeGroup> + </EuiFlexItem> </EuiFlexGroup> ); }; @@ -83,43 +95,39 @@ export const ChooseConnectorSelectable: React.FC<ChooseConnectorSelectableProps> const getInitialOptions = () => { return allConnectors.map((connector, key) => { const _append: JSX.Element[] = []; + let _ariaLabelAppend = ''; if (connector.isTechPreview) { _append.push( - <EuiBadge key={key + '-preview'} iconType="beaker" color="hollow"> - {i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel', - { defaultMessage: 'Tech preview' } - )} + <EuiBadge + aria-label={TECH_PREVIEW_LABEL} + key={key + '-preview'} + iconType="beaker" + color="hollow" + > + {TECH_PREVIEW_LABEL} </EuiBadge> ); + _ariaLabelAppend += `, ${TECH_PREVIEW_LABEL}`; } if (connector.isBeta) { _append.push( - <EuiBadge key={key + '-beta'} iconType={'beta'} color="hollow"> - {i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel', - { - defaultMessage: 'Beta', - } - )} + <EuiBadge aria-label={BETA_LABEL} key={key + '-beta'} iconType={'beta'} color="hollow"> + {BETA_LABEL} </EuiBadge> ); + _ariaLabelAppend += `, ${BETA_LABEL}`; } if (selfManaged === 'native' && !connector.isNative) { _append.push( <EuiBadge key={key + '-self'} iconType={'warning'} color="warning"> - {i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel', - { - defaultMessage: 'Self managed', - } - )} + {CONNECTOR_CLIENT_LABEL} </EuiBadge> ); } return { _append, _prepend: <EuiIcon size="l" type={connector.iconPath} />, + 'aria-label': connector.name + _ariaLabelAppend, key: key.toString(), label: connector.name, }; @@ -133,33 +141,31 @@ export const ChooseConnectorSelectable: React.FC<ChooseConnectorSelectableProps> }, [selfManaged]); return ( - <EuiFlexItem> - <EuiComboBox - aria-label={i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.euiComboBox.accessibleScreenReaderLabelLabel', - { defaultMessage: 'Select a data source for your connector to use.' } - )} - prepend={<EuiIcon type={selectedConnector?.iconPath ?? connectorLogo} size="l" />} - singleSelection - fullWidth - placeholder={i18n.translate( - 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', - { defaultMessage: 'Choose a data source' } - )} - options={selectableOptions} - selectedOptions={selectedOption} - onChange={(selectedItem) => { - setSelectedOption(selectedItem); - if (selectedItem.length === 0) { - setSelectedConnector(null); - return; - } - const keySelected = Number(selectedItem[0].key); - setSelectedConnector(allConnectors[keySelected]); - }} - renderOption={renderOption} - rowHeight={(euiTheme.base / 2) * 5} - /> - </EuiFlexItem> + <EuiComboBox + aria-label={i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.euiComboBox.accessibleScreenReaderLabelLabel', + { defaultMessage: 'Select a data source for your connector to use.' } + )} + prepend={<EuiIcon type={selectedConnector?.iconPath ?? connectorLogo} size="l" />} + singleSelection + fullWidth + placeholder={i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', + { defaultMessage: 'Choose a data source' } + )} + options={selectableOptions} + selectedOptions={selectedOption} + onChange={(selectedItem) => { + setSelectedOption(selectedItem); + if (selectedItem.length === 0) { + setSelectedConnector(null); + return; + } + const keySelected = Number(selectedItem[0].key); + setSelectedConnector(allConnectors[keySelected]); + }} + renderOption={renderOption} + rowHeight={(euiTheme.base / 2) * 5} + /> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx index 7e23474b207f1..46840d577e4d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx @@ -20,6 +20,7 @@ import { EuiRadio, EuiSpacer, EuiText, + useIsWithinBreakpoints, EuiTitle, useGeneratedHtmlId, } from '@elastic/eui'; @@ -33,7 +34,7 @@ import { GeneratedConfigFields } from '../../connector_detail/components/generat import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; import { NewConnectorLogic } from '../../new_index/method_connector/new_connector_logic'; -import { ChooseConnectorSelectable } from './components/choose_connector_selectable'; +import { ChooseConnector } from './components/choose_connector'; import { ConnectorDescriptionPopover } from './components/connector_description_popover'; import { ManualConfiguration } from './components/manual_configuration'; import { SelfManagePreference } from './create_connector'; @@ -53,6 +54,7 @@ export const StartStep: React.FC<StartStepProps> = ({ onSelfManagePreferenceChange, error, }) => { + const isMediumDevice = useIsWithinBreakpoints(['xs', 's', 'm', 'l']); const elasticManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'elasticManagedRadioButton' }); const selfManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'selfManagedRadioButton' }); @@ -93,8 +95,8 @@ export const StartStep: React.FC<StartStepProps> = ({ <h3>{title}</h3> </EuiTitle> <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem> + <EuiFlexGroup direction={isMediumDevice ? 'column' : 'row'}> + <EuiFlexItem grow={7}> <EuiFormRow fullWidth label={i18n.translate( @@ -102,10 +104,10 @@ export const StartStep: React.FC<StartStepProps> = ({ { defaultMessage: 'Connector' } )} > - <ChooseConnectorSelectable selfManaged={selfManagePreference} /> + <ChooseConnector selfManaged={selfManagePreference} /> </EuiFormRow> </EuiFlexItem> - <EuiFlexItem> + <EuiFlexItem grow={5}> <EuiFormRow fullWidth isInvalid={!!error} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx index 1ed3857b2c7ce..9d3daac23111f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx @@ -31,15 +31,10 @@ export interface IndexErrorProps { } interface SemanticTextProperty extends MappingPropertyBase { - inference_id?: string; + inference_id: string; type: 'semantic_text'; } -/* - This will be repalce once we add default elser inference_id - with the index mapping response. -*/ -const ELSER_PRECONFIGURED_ENDPOINTS = '.elser-2-elasticsearch'; const isInferencePreconfigured = (inferenceId: string) => inferenceId.startsWith('.'); const parseMapping = (mappings: MappingTypeMapping) => { @@ -56,11 +51,6 @@ const getSemanticTextFields = ( ): Array<{ path: string; source: SemanticTextProperty }> => { return Object.entries(fields).flatMap(([key, value]) => { const currentPath: string = path ? `${path}.${key}` : key; - if (value.type === 'semantic_text') { - value = value.inference_id - ? value - : { ...value, inference_id: ELSER_PRECONFIGURED_ENDPOINTS }; - } const currentField: Array<{ path: string; source: SemanticTextProperty }> = value.type === 'semantic_text' ? [{ path: currentPath, source: value }] : []; if (hasProperties(value)) { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/default_settings_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/default_settings_flyout.tsx index 3c109e2135726..e62c08f3cdaa9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/default_settings_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/default_settings_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useRef } from 'react'; import { useValues, useActions } from 'kea'; @@ -56,6 +56,8 @@ export const DefaultSettingsFlyout: React.FC<DefaultSettingsFlyoutProps> = ({ cl reduce_whitespace: reduceWhitespace, run_ml_inference: runMLInference, } = pipelineState; + // Reference the first focusable element in the flyout for accessibility on click or Enter key action either Reset or Save button + const firstFocusInFlyoutRef = useRef<HTMLAnchorElement>(null); return ( <EuiFlyout onClose={closeFlyout} size="s" paddingSize="l"> <EuiFlyoutHeader hasBorder> @@ -81,6 +83,7 @@ export const DefaultSettingsFlyout: React.FC<DefaultSettingsFlyoutProps> = ({ cl data-telemetry-id="entSearchContent-defaultSettingsFlyout-ingestPipelinesLink" href={docLinks.ingestPipelines} target="_blank" + ref={firstFocusInFlyoutRef} > {i18n.translate( 'xpack.enterpriseSearch.defaultSettingsFlyout.body.description.ingestPipelinesLink.link', @@ -204,7 +207,10 @@ export const DefaultSettingsFlyout: React.FC<DefaultSettingsFlyoutProps> = ({ cl color="primary" disabled={hasNoChanges} isLoading={isLoading} - onClick={() => setPipeline(defaultPipeline)} + onClick={() => { + setPipeline(defaultPipeline); + firstFocusInFlyoutRef.current?.focus(); + }} data-test-subj={'entSearchContentSettingsResetButton'} > {i18n.translate('xpack.enterpriseSearch.content.settings.resetButtonLabel', { @@ -218,7 +224,10 @@ export const DefaultSettingsFlyout: React.FC<DefaultSettingsFlyoutProps> = ({ cl fill disabled={hasNoChanges} isLoading={isLoading} - onClick={() => makeRequest(pipelineState)} + onClick={() => { + makeRequest(pipelineState); + firstFocusInFlyoutRef.current?.focus(); + }} data-test-subj={'entSearchContentSettingsSaveButton'} > {i18n.translate('xpack.enterpriseSearch.content.settings.saveButtonLabel', { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts index f1da7cefa5cc0..dd4e13a6df278 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts @@ -27,6 +27,10 @@ export const BETA_LABEL = i18n.translate('xpack.enterpriseSearch.betaLabel', { defaultMessage: 'Beta', }); +export const TECH_PREVIEW_LABEL = i18n.translate('xpack.enterpriseSearch.techPreviewLabel', { + defaultMessage: 'Tech preview', +}); + export const NATIVE_LABEL = i18n.translate('xpack.enterpriseSearch.nativeLabel', { defaultMessage: 'Elastic managed', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx index b971ab6deff53..a8fff53d8a9b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/base_nav.tsx @@ -17,10 +17,11 @@ import { SEARCH_AI_SEARCH, } from '@kbn/deeplinks-search'; import { i18n } from '@kbn/i18n'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; import { GETTING_STARTED_TITLE } from '../../../../common/constants'; -import { ClassicNavItem, BuildClassicNavParameters } from '../types'; +import { BuildClassicNavParameters } from '../types'; export const buildBaseClassicNavItems = ({ productAccess, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts index 514072ba297aa..d43d14aba2235 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.test.ts @@ -8,6 +8,7 @@ import { mockKibanaValues } from '../../__mocks__/kea_logic'; import type { ChromeNavLink } from '@kbn/core-chrome-browser'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; import '../../__mocks__/react_router'; @@ -15,8 +16,6 @@ jest.mock('../react_router_helpers/link_events', () => ({ letBrowserHandleEvent: jest.fn(), })); -import { ClassicNavItem } from '../types'; - import { generateSideNavItems } from './classic_nav_helpers'; describe('generateSideNavItems', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts index 89f3c2ab5b59a..4609e01beb6f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/classic_nav_helpers.ts @@ -6,12 +6,9 @@ */ import { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; +import type { ClassicNavItem } from '@kbn/search-navigation/public'; -import { - ClassicNavItem, - GenerateNavLinkFromDeepLinkParameters, - GenerateNavLinkParameters, -} from '../types'; +import type { GenerateNavLinkFromDeepLinkParameters, GenerateNavLinkParameters } from '../types'; import { generateNavLink } from './nav_link_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 3305e92dd8d9e..a6cbf56691735 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -101,7 +101,7 @@ const baseNavItems = [ items: [ { 'data-test-subj': 'searchSideNav-InferenceEndpoints', - href: '/app/enterprise_search/relevance/inference_endpoints', + href: '/app/elasticsearch/relevance/inference_endpoints', id: 'inference_endpoints', items: undefined, name: 'Inference Endpoints', @@ -205,7 +205,7 @@ const mockNavLinks = [ { id: 'searchInferenceEndpoints:inferenceEndpoints', title: 'Inference Endpoints', - url: '/app/enterprise_search/relevance/inference_endpoints', + url: '/app/elasticsearch/relevance/inference_endpoints', }, { id: 'appSearch:engines', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 095f1dddfcc4a..25fce6c62d05d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { ReactNode } from 'react'; - import type { AppDeepLinkId, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; @@ -87,12 +85,3 @@ export interface GenerateNavLinkFromDeepLinkParameters { export interface BuildClassicNavParameters { productAccess: ProductAccess; } - -export interface ClassicNavItem { - 'data-test-subj'?: string; - deepLink?: GenerateNavLinkFromDeepLinkParameters; - iconToString?: string; - id: string; - items?: ClassicNavItem[]; - name?: ReactNode; -} diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 4d2c66eee2e93..4d357956f6bb5 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -35,6 +35,7 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public' import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { SearchConnectorsPluginStart } from '@kbn/search-connectors-plugin/public'; import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; +import type { SearchNavigationPluginStart } from '@kbn/search-navigation/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; @@ -55,7 +56,7 @@ import { SEARCH_RELEVANCE_PLUGIN, } from '../common/constants'; import { registerLocators } from '../common/locators'; -import { ClientConfigType, InitialAppData } from '../common/types'; +import { ClientConfigType, InitialAppData, ProductAccess } from '../common/types'; import { hasEnterpriseLicense } from '../common/utils/licensing'; import { ENGINES_PATH } from './applications/app_search/routes'; @@ -99,6 +100,7 @@ export interface PluginsStart { navigation: NavigationPublicPluginStart; searchConnectors?: SearchConnectorsPluginStart; searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; + searchNavigation?: SearchNavigationPluginStart; searchPlayground?: SearchPlaygroundPluginStart; security?: SecurityPluginStart; share?: SharePluginStart; @@ -618,6 +620,27 @@ export class EnterpriseSearchPlugin implements Plugin { }) ); }); + if (plugins.searchNavigation !== undefined) { + // while we have ent-search apps in the side nav, we need to provide access + // to the base set of classic side nav items to the search-navigation plugin. + import('./applications/shared/layout/base_nav').then(({ buildBaseClassicNavItems }) => { + plugins.searchNavigation?.setGetBaseClassicNavItems(() => { + const productAccess: ProductAccess = this.data?.access ?? { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }; + + return buildBaseClassicNavItems({ productAccess }); + }); + }); + + // This is needed so that we can fetch product access for plugins + // that need to share the classic nav. This can be removed when we + // remove product access and ent-search apps. + plugins.searchNavigation.registerOnAppMountHandler(async () => { + return this.getInitialData(core.http); + }); + } plugins.licensing?.license$.subscribe((license) => { if (hasEnterpriseLicense(license)) { diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 7b7556729a76c..de98a647e0a94 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -83,6 +83,7 @@ "@kbn/security-plugin-types-common", "@kbn/core-security-server", "@kbn/core-security-server-mocks", - "@kbn/unsaved-changes-prompt" + "@kbn/unsaved-changes-prompt", + "@kbn/search-navigation", ] } diff --git a/x-pack/plugins/entity_manager/kibana.jsonc b/x-pack/plugins/entity_manager/kibana.jsonc index d5dadcf8fd2b7..c18822d48ac0a 100644 --- a/x-pack/plugins/entity_manager/kibana.jsonc +++ b/x-pack/plugins/entity_manager/kibana.jsonc @@ -8,9 +8,13 @@ "plugin": { "id": "entityManager", "configPath": ["xpack", "entityManager"], - "requiredPlugins": ["security", "encryptedSavedObjects", "licensing"], "browser": true, "server": true, + "requiredPlugins": [ + "security", + "encryptedSavedObjects", + "licensing" + ], "requiredBundles": [] } } diff --git a/x-pack/plugins/entity_manager/public/index.ts b/x-pack/plugins/entity_manager/public/index.ts index 85a9285b1692d..73d23ad45e9c1 100644 --- a/x-pack/plugins/entity_manager/public/index.ts +++ b/x-pack/plugins/entity_manager/public/index.ts @@ -16,6 +16,8 @@ export const plugin: PluginInitializer< return new Plugin(context); }; +export { EntityClient } from './lib/entity_client'; + export type { EntityManagerPublicPluginSetup, EntityManagerPublicPluginStart }; export type EntityManagerAppId = 'entityManager'; diff --git a/x-pack/plugins/entity_manager/public/plugin.ts b/x-pack/plugins/entity_manager/public/plugin.ts index 6d6d56a95b757..7ff6354c997eb 100644 --- a/x-pack/plugins/entity_manager/public/plugin.ts +++ b/x-pack/plugins/entity_manager/public/plugin.ts @@ -22,16 +22,14 @@ export class Plugin implements EntityManagerPluginClass { } setup(core: CoreSetup) { - const entityClient = new EntityClient(core); return { - entityClient, + entityClient: new EntityClient(core), }; } start(core: CoreStart) { - const entityClient = new EntityClient(core); return { - entityClient, + entityClient: new EntityClient(core), }; } diff --git a/x-pack/plugins/entity_manager/public/types.ts b/x-pack/plugins/entity_manager/public/types.ts index 66499479299dc..90d9026e8b9ba 100644 --- a/x-pack/plugins/entity_manager/public/types.ts +++ b/x-pack/plugins/entity_manager/public/types.ts @@ -10,7 +10,6 @@ import type { EntityClient } from './lib/entity_client'; export interface EntityManagerPublicPluginSetup { entityClient: EntityClient; } - export interface EntityManagerPublicPluginStart { entityClient: EntityClient; } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/cron_job.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/cron_job.ts index 7849dcdc73f5b..06bc9dba9fce7 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/cron_job.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/cron_job.ts @@ -13,7 +13,7 @@ import { commonEcsMetadata } from '../common/ecs_metadata'; export const builtInKubernetesCronJobEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_cron_job_ecs`, - filter: 'kubernetes.cronjob.uid : *', + filter: 'kubernetes.cronjob.name : *', managed: true, version: '0.1.0', name: 'Kubernetes CronJob from ECS data', @@ -21,7 +21,7 @@ export const builtInKubernetesCronJobEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes cron job entities from the Kubernetes integration data streams', type: 'k8s.cronjob.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.cronjob.uid'], + identityFields: ['kubernetes.cronjob.name'], displayNameTemplate: '{{kubernetes.cronjob.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/daemon_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/daemon_set.ts index 5b57cdd6ae2f8..c69a1c5c7e625 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/daemon_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/daemon_set.ts @@ -13,7 +13,7 @@ import { commonEcsMetadata } from '../common/ecs_metadata'; export const builtInKubernetesDaemonSetEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_daemon_set_ecs`, - filter: 'kubernetes.daemonset.uid : *', + filter: 'kubernetes.daemonset.name : *', managed: true, version: '0.1.0', name: 'Kubernetes DaemonSet from ECS data', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/deployment.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/deployment.ts index d33c14db7e2c9..f8e8f920e2f47 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/deployment.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/deployment.ts @@ -13,7 +13,7 @@ import { commonEcsIndexPatterns } from '../common/ecs_index_patterns'; export const builtInKubernetesDeploymentEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_deployment_ecs`, - filter: 'kubernetes.deployment.uid : *', + filter: 'kubernetes.deployment.name : *', managed: true, version: '0.1.0', name: 'Kubernetes Deployment from ECS data', @@ -21,7 +21,7 @@ export const builtInKubernetesDeploymentEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes deployment entities from the Kubernetes integration data streams', type: 'k8s.deployment.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.deployment.uid'], + identityFields: ['kubernetes.deployment.name'], displayNameTemplate: '{{kubernetes.deployment.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/job.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/job.ts index 92c6d13251553..4efc41dc9ea81 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/job.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/job.ts @@ -13,7 +13,7 @@ import { commonEcsMetadata } from '../common/ecs_metadata'; export const builtInKubernetesJobEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_job_ecs`, - filter: 'kubernetes.job.uid : *', + filter: 'kubernetes.job.name : *', managed: true, version: '0.1.0', name: 'Kubernetes Job from ECS data', @@ -21,7 +21,7 @@ export const builtInKubernetesJobEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes job entities from the Kubernetes integration data streams', type: 'k8s.job.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.job.uid'], + identityFields: ['kubernetes.job.name'], displayNameTemplate: '{{kubernetes.job.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/node.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/node.ts index f3fdcdfaf04b4..033bd8313928d 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/node.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/node.ts @@ -13,7 +13,7 @@ import { commonEcsMetadata } from '../common/ecs_metadata'; export const builtInKubernetesNodeEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_node_ecs`, - filer: 'kubernetes.node.uid : *', + filer: 'kubernetes.node.name : *', managed: true, version: '0.1.0', name: 'Kubernetes Node from ECS data', @@ -21,7 +21,7 @@ export const builtInKubernetesNodeEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes node entities from the Kubernetes integration data streams', type: 'k8s.node.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.node.uid'], + identityFields: ['kubernetes.node.name'], displayNameTemplate: '{{kubernetes.node.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/pod.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/pod.ts index 7aa53da6e5a7d..32029617d992c 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/pod.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/pod.ts @@ -21,7 +21,7 @@ export const builtInKubernetesPodEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes pod entities from the Kubernetes integration data streams', type: 'k8s.pod.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.pod.name'], + identityFields: ['kubernetes.pod.uid'], displayNameTemplate: '{{kubernetes.pod.name}}', latest: { timestampField: '@timestamp', @@ -30,5 +30,12 @@ export const builtInKubernetesPodEcsEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonEcsMetadata, + metadata: [ + ...commonEcsMetadata, + { + source: 'kubernetes.pod.name', + destination: 'kubernetes.pod.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/replica_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/replica_set.ts index cc059c14979d0..e9f534be8f1db 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/replica_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/replica_set.ts @@ -13,6 +13,7 @@ import { commonEcsIndexPatterns } from '../common/ecs_index_patterns'; export const builtInKubernetesReplicaSetEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_replica_set_ecs`, + filer: 'kubernetes.replicaset.name : *', managed: true, version: '0.1.0', name: 'Kubernetes ReplicaSet from ECS data', @@ -20,7 +21,7 @@ export const builtInKubernetesReplicaSetEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes replica set entities from the Kubernetes integration data streams', type: 'k8s.replicaset.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.replicaset.uid'], + identityFields: ['kubernetes.replicaset.name'], displayNameTemplate: '{{kubernetes.replicaset.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/stateful_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/stateful_set.ts index 79f9d4489216f..927c8a259276d 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/stateful_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/ecs/stateful_set.ts @@ -13,7 +13,7 @@ import { commonEcsIndexPatterns } from '../common/ecs_index_patterns'; export const builtInKubernetesStatefulSetEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}kubernetes_stateful_set_ecs`, - filter: 'kubernetes.statefulset.uid : *', + filter: 'kubernetes.statefulset.name : *', managed: true, version: '0.1.0', name: 'Kubernetes StatefulSet from ECS data', @@ -21,7 +21,7 @@ export const builtInKubernetesStatefulSetEcsEntityDefinition: EntityDefinition = 'This definition extracts Kubernetes stateful set entities from the Kubernetes integration data streams', type: 'k8s.statefulset.ecs', indexPatterns: commonEcsIndexPatterns, - identityFields: ['kubernetes.statefulset.uid'], + identityFields: ['kubernetes.statefulset.name'], displayNameTemplate: '{{kubernetes.statefulset.name}}', latest: { timestampField: '@timestamp', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cluster.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cluster.ts index 0ec244ec617f3..71024cfb166f2 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cluster.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cluster.ts @@ -30,5 +30,12 @@ export const builtInKubernetesClusterSemConvEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.cluster.name', + destination: 'k8s.cluster.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cron_job.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cron_job.ts index 6d677943976d1..fff257bcf8e57 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cron_job.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/cron_job.ts @@ -30,5 +30,12 @@ export const builtInKubernetesCronJobSemConvEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.cronjob.name', + destination: 'k8s.cronjob.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/daemon_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/daemon_set.ts index a4b61933ad316..cf89dcc30e671 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/daemon_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/daemon_set.ts @@ -30,5 +30,12 @@ export const builtInKubernetesDaemonSetSemConvEntityDefinition: EntityDefinition frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.daemonset.name', + destination: 'k8s.daemonset.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/deployment.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/deployment.ts index bdb3cb1cef59b..05a89d67ead33 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/deployment.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/deployment.ts @@ -30,5 +30,12 @@ export const builtInKubernetesDeploymentSemConvEntityDefinition: EntityDefinitio frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.deployment.name', + destination: 'k8s.deployment.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/job.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/job.ts index b2e48cf7494fb..557afa54ca55e 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/job.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/job.ts @@ -30,5 +30,12 @@ export const builtInKubernetesJobSemConvEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.job.name', + destination: 'k8s.job.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/node.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/node.ts index 456f030421075..35bbed42e6a4a 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/node.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/node.ts @@ -22,7 +22,7 @@ export const builtInKubernetesNodeSemConvEntityDefinition: EntityDefinition = type: 'k8s.node.otel', indexPatterns: commonOtelIndexPatterns, identityFields: ['k8s.node.uid'], - displayNameTemplate: '{{k8s.node.uid}}', + displayNameTemplate: '{{k8s.node.name}}', latest: { timestampField: '@timestamp', lookbackPeriod: '10m', @@ -30,5 +30,12 @@ export const builtInKubernetesNodeSemConvEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.node.name', + destination: 'k8s.node.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/pod.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/pod.ts index 6dc879d761dd8..05d22163fbacc 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/pod.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/pod.ts @@ -30,5 +30,12 @@ export const builtInKubernetesPodSemConvEntityDefinition: EntityDefinition = frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.pod.name', + destination: 'k8s.pod.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/replica_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/replica_set.ts index 47bad6bf8a641..ff4e33d789da9 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/replica_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/replica_set.ts @@ -19,9 +19,9 @@ export const builtInKubernetesReplicaSetSemConvEntityDefinition: EntityDefinitio name: 'Kubernetes ReplicaSet from SemConv data', description: 'This definition extracts Kubernetes replica set entities using data collected with OpenTelemetry', - type: 'kubernetes_replica_set_semconv', + type: 'k8s.replicaset.otel', indexPatterns: commonOtelIndexPatterns, - identityFields: ['k8s.replicaset.name'], + identityFields: ['k8s.replicaset.uid'], displayNameTemplate: '{{k8s.replicaset.name}}', latest: { timestampField: '@timestamp', @@ -30,5 +30,12 @@ export const builtInKubernetesReplicaSetSemConvEntityDefinition: EntityDefinitio frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.replicaset.name', + destination: 'k8s.replicaset.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/stateful_set.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/stateful_set.ts index c61d7e5d965cd..9c8b385f05c76 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/stateful_set.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/kubernetes/semconv/stateful_set.ts @@ -30,5 +30,12 @@ export const builtInKubernetesStatefulSetSemConvEntityDefinition: EntityDefiniti frequency: '5m', }, }, - metadata: commonOtelMetadata, + metadata: [ + ...commonOtelMetadata, + { + source: 'k8s.statefulset.name', + destination: 'k8s.statefulset.name', + aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, + }, + ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/errors/unknown_entity_type.ts b/x-pack/plugins/entity_manager/server/lib/entities/errors/unknown_entity_type.ts new file mode 100644 index 0000000000000..5d29e24a1cca2 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/entities/errors/unknown_entity_type.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class UnknownEntityType extends Error { + constructor(message: string) { + super(message); + this.name = 'UnknownEntityType'; + } +} diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts index 8bb51941092f2..7045bee1fc538 100644 --- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema'; +import { EntityV2, EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; @@ -23,6 +23,9 @@ import { stopTransforms } from './entities/stop_transforms'; import { deleteIndices } from './entities/delete_index'; import { EntityDefinitionWithState } from './entities/types'; import { EntityDefinitionUpdateConflict } from './entities/errors/entity_definition_update_conflict'; +import { EntitySource, getEntityInstancesQuery } from './queries'; +import { mergeEntitiesList, runESQLQuery } from './queries/utils'; +import { UnknownEntityType } from './entities/errors/unknown_entity_type'; export class EntityClient { constructor( @@ -126,8 +129,6 @@ export class EntityClient { }); if (deleteData) { - // delete data with current user as system user does not have - // .entities privileges await deleteIndices(this.options.esClient, definition, this.options.logger); } } @@ -170,4 +171,114 @@ export class EntityClient { this.options.logger.info(`Stopping transforms for definition [${definition.id}]`); return stopTransforms(this.options.esClient, definition, this.options.logger); } + + async getEntitySources({ type }: { type: string }) { + const result = await this.options.esClient.search<EntitySource>({ + index: 'kibana_entity_definitions', + query: { + bool: { + must: { + term: { entity_type: type }, + }, + }, + }, + }); + + return result.hits.hits.map((hit) => hit._source) as EntitySource[]; + } + + async searchEntities({ + type, + start, + end, + metadataFields = [], + filters = [], + limit = 10, + }: { + type: string; + start: string; + end: string; + metadataFields?: string[]; + filters?: string[]; + limit?: number; + }) { + const sources = await this.getEntitySources({ type }); + if (sources.length === 0) { + throw new UnknownEntityType(`No sources found for entity type [${type}]`); + } + + return this.searchEntitiesBySources({ + sources, + start, + end, + metadataFields, + filters, + limit, + }); + } + + async searchEntitiesBySources({ + sources, + start, + end, + metadataFields = [], + filters = [], + limit = 10, + }: { + sources: EntitySource[]; + start: string; + end: string; + metadataFields?: string[]; + filters?: string[]; + limit?: number; + }) { + const entities = await Promise.all( + sources.map(async (source) => { + const mandatoryFields = [source.timestamp_field, ...source.identity_fields]; + const metaFields = [...metadataFields, ...source.metadata_fields]; + const { fields } = await this.options.esClient.fieldCaps({ + index: source.index_patterns, + fields: [...mandatoryFields, ...metaFields], + }); + + const sourceHasMandatoryFields = mandatoryFields.every((field) => !!fields[field]); + if (!sourceHasMandatoryFields) { + // we can't build entities without id fields so we ignore the source. + // filters should likely behave similarly. + this.options.logger.info( + `Ignoring source for type [${source.type}] with index_patterns [${source.index_patterns}] because some mandatory fields [${mandatoryFields}] are not mapped` + ); + return []; + } + + // but metadata field not being available is fine + const availableMetadataFields = metaFields.filter((field) => fields[field]); + + const query = getEntityInstancesQuery({ + source: { + ...source, + metadata_fields: availableMetadataFields, + filters: [...source.filters, ...filters], + }, + start, + end, + limit, + }); + this.options.logger.debug(`Entity query: ${query}`); + + const rawEntities = await runESQLQuery<EntityV2>({ + query, + esClient: this.options.esClient, + }); + + return rawEntities.map((entity) => { + entity['entity.id'] = source.identity_fields.map((field) => entity[field]).join(':'); + entity['entity.type'] = source.type; + return entity; + }); + }) + ).then((results) => results.flat()); + + return mergeEntitiesList(entities).slice(0, limit); + } } diff --git a/x-pack/plugins/entity_manager/server/lib/queries/index.test.ts b/x-pack/plugins/entity_manager/server/lib/queries/index.test.ts new file mode 100644 index 0000000000000..539d20c464794 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/queries/index.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getEntityInstancesQuery } from '.'; + +describe('getEntityInstancesQuery', () => { + describe('getEntityInstancesQuery', () => { + it('generates a valid esql query', () => { + const query = getEntityInstancesQuery({ + source: { + type: 'service', + index_patterns: ['logs-*', 'metrics-*'], + identity_fields: ['service.name'], + metadata_fields: ['host.name'], + filters: [], + timestamp_field: 'custom_timestamp_field', + }, + limit: 5, + start: '2024-11-20T19:00:00.000Z', + end: '2024-11-20T20:00:00.000Z', + }); + + expect(query).toEqual( + 'FROM logs-*,metrics-*|' + + 'WHERE custom_timestamp_field >= "2024-11-20T19:00:00.000Z"|' + + 'WHERE custom_timestamp_field <= "2024-11-20T20:00:00.000Z"|' + + 'WHERE service.name IS NOT NULL|' + + 'STATS entity.last_seen_timestamp=MAX(custom_timestamp_field),metadata.host.name=VALUES(host.name) BY service.name|' + + 'SORT entity.last_seen_timestamp DESC|' + + 'LIMIT 5' + ); + }); + }); +}); diff --git a/x-pack/plugins/entity_manager/server/lib/queries/index.ts b/x-pack/plugins/entity_manager/server/lib/queries/index.ts new file mode 100644 index 0000000000000..9fc7ae00c9aa6 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/queries/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; + +export const entitySourceSchema = z.object({ + type: z.string(), + timestamp_field: z.optional(z.string()).default('@timestamp'), + index_patterns: z.array(z.string()), + identity_fields: z.array(z.string()), + metadata_fields: z.array(z.string()), + filters: z.array(z.string()), +}); + +export type EntitySource = z.infer<typeof entitySourceSchema>; + +const sourceCommand = ({ source }: { source: EntitySource }) => { + let query = `FROM ${source.index_patterns}`; + + const esMetadataFields = source.metadata_fields.filter((field) => + ['_index', '_id'].includes(field) + ); + if (esMetadataFields.length) { + query += ` METADATA ${esMetadataFields.join(',')}`; + } + + return query; +}; + +const filterCommands = ({ + source, + start, + end, +}: { + source: EntitySource; + start: string; + end: string; +}) => { + const commands = [ + `WHERE ${source.timestamp_field} >= "${start}"`, + `WHERE ${source.timestamp_field} <= "${end}"`, + ]; + + source.identity_fields.forEach((field) => { + commands.push(`WHERE ${field} IS NOT NULL`); + }); + + source.filters.forEach((filter) => { + commands.push(`WHERE ${filter}`); + }); + + return commands; +}; + +const statsCommand = ({ source }: { source: EntitySource }) => { + const aggs = [ + // default 'last_seen' attribute + `entity.last_seen_timestamp=MAX(${source.timestamp_field})`, + ...source.metadata_fields + .filter((field) => !source.identity_fields.some((idField) => idField === field)) + .map((field) => `metadata.${field}=VALUES(${field})`), + ]; + + return `STATS ${aggs.join(',')} BY ${source.identity_fields.join(',')}`; +}; + +export function getEntityInstancesQuery({ + source, + limit, + start, + end, +}: { + source: EntitySource; + limit: number; + start: string; + end: string; +}): string { + const commands = [ + sourceCommand({ source }), + ...filterCommands({ source, start, end }), + statsCommand({ source }), + `SORT entity.last_seen_timestamp DESC`, + `LIMIT ${limit}`, + ]; + + return commands.join('|'); +} diff --git a/x-pack/plugins/entity_manager/server/lib/queries/utils.test.ts b/x-pack/plugins/entity_manager/server/lib/queries/utils.test.ts new file mode 100644 index 0000000000000..5d57025671726 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/queries/utils.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mergeEntitiesList } from './utils'; + +describe('mergeEntitiesList', () => { + describe('mergeEntitiesList', () => { + it('merges entities on entity.id', () => { + const entities = [ + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + }, + ]; + + const mergedEntities = mergeEntitiesList(entities); + expect(mergedEntities.length).toEqual(1); + expect(mergedEntities[0]).toEqual({ + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + }); + }); + + it('merges metadata fields', () => { + const entities = [ + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-1', + 'metadata.agent.name': 'agent-1', + 'metadata.service.environment': ['dev', 'staging'], + 'metadata.only_in_record_1': 'foo', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': ['host-2', 'host-3'], + 'metadata.agent.name': 'agent-2', + 'metadata.service.environment': 'prod', + 'metadata.only_in_record_2': 'bar', + }, + ]; + + const mergedEntities = mergeEntitiesList(entities); + expect(mergedEntities.length).toEqual(1); + expect(mergedEntities[0]).toEqual({ + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': ['host-1', 'host-2', 'host-3'], + 'metadata.agent.name': ['agent-1', 'agent-2'], + 'metadata.service.environment': ['dev', 'staging', 'prod'], + 'metadata.only_in_record_1': 'foo', + 'metadata.only_in_record_2': 'bar', + }); + }); + + it('picks most recent timestamp when merging', () => { + const entities = [ + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-1', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-2', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T16:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-3', + }, + ]; + + const mergedEntities = mergeEntitiesList(entities); + expect(mergedEntities.length).toEqual(1); + expect(mergedEntities[0]).toEqual({ + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': ['host-1', 'host-2', 'host-3'], + }); + }); + + it('deduplicates metadata values', () => { + const entities = [ + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T18:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-1', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': 'host-2', + }, + { + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T16:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': ['host-1', 'host-2'], + }, + ]; + + const mergedEntities = mergeEntitiesList(entities); + expect(mergedEntities.length).toEqual(1); + expect(mergedEntities[0]).toEqual({ + 'entity.id': 'foo', + 'entity.last_seen_timestamp': '2024-11-20T20:00:00.000Z', + 'entity.type': 'service', + 'metadata.host.name': ['host-1', 'host-2'], + }); + }); + }); +}); diff --git a/x-pack/plugins/entity_manager/server/lib/queries/utils.ts b/x-pack/plugins/entity_manager/server/lib/queries/utils.ts new file mode 100644 index 0000000000000..68f5b0f11aff2 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/queries/utils.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { EntityV2 } from '@kbn/entities-schema'; +import { ESQLSearchResponse } from '@kbn/es-types'; +import { uniq } from 'lodash'; + +function mergeEntities(entity1: EntityV2, entity2: EntityV2): EntityV2 { + const merged: EntityV2 = { + ...entity1, + 'entity.last_seen_timestamp': new Date( + Math.max( + Date.parse(entity1['entity.last_seen_timestamp']), + Date.parse(entity2['entity.last_seen_timestamp']) + ) + ).toISOString(), + }; + + for (const [key, value] of Object.entries(entity2).filter(([_key]) => + _key.startsWith('metadata.') + )) { + if (merged[key]) { + merged[key] = uniq([ + ...(Array.isArray(merged[key]) ? merged[key] : [merged[key]]), + ...(Array.isArray(value) ? value : [value]), + ]); + } else { + merged[key] = value; + } + } + return merged; +} + +export function mergeEntitiesList(entities: EntityV2[]): EntityV2[] { + const instances: { [key: string]: EntityV2 } = {}; + + for (let i = 0; i < entities.length; i++) { + const entity = entities[i]; + const id = entity['entity.id']; + + if (instances[id]) { + instances[id] = mergeEntities(instances[id], entity); + } else { + instances[id] = entity; + } + } + + return Object.values(instances); +} + +export async function runESQLQuery<T>({ + esClient, + query, +}: { + esClient: ElasticsearchClient; + query: string; +}): Promise<T[]> { + const esqlResponse = (await esClient.esql.query( + { + query, + format: 'json', + }, + { querystring: { drop_null_columns: true } } + )) as unknown as ESQLSearchResponse; + + const documents = esqlResponse.values.map((row) => + row.reduce<Record<string, any>>((acc, value, index) => { + const column = esqlResponse.columns[index]; + + if (!column) { + return acc; + } + + // Removes the type suffix from the column name + const name = column.name.replace(/\.(text|keyword)$/, ''); + if (!acc[name]) { + acc[name] = value; + } + + return acc; + }, {}) + ) as T[]; + + return documents; +} diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/check.ts b/x-pack/plugins/entity_manager/server/routes/enablement/check.ts index 5373ac9df50f5..100b0ac382dcf 100644 --- a/x-pack/plugins/entity_manager/server/routes/enablement/check.ts +++ b/x-pack/plugins/entity_manager/server/routes/enablement/check.ts @@ -45,13 +45,11 @@ import { createEntityManagerServerRoute } from '../create_entity_manager_server_ */ export const checkEntityDiscoveryEnabledRoute = createEntityManagerServerRoute({ endpoint: 'GET /internal/entities/managed/enablement', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', }, }, handler: async ({ response, logger, server }) => { diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts index a71a317045c44..1c8755c682f7f 100644 --- a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts +++ b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts @@ -44,13 +44,11 @@ import { createEntityManagerServerRoute } from '../create_entity_manager_server_ */ export const disableEntityDiscoveryRoute = createEntityManagerServerRoute({ endpoint: 'DELETE /internal/entities/managed/enablement', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts index 562b798a598a6..6ddb65804b90f 100644 --- a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts +++ b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts @@ -63,13 +63,11 @@ import { startTransforms } from '../../lib/entities/start_transforms'; */ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({ endpoint: 'PUT /internal/entities/managed/enablement', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint leverages the security plugin to evaluate the privileges needed as part of its core flow', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/entities/create.ts b/x-pack/plugins/entity_manager/server/routes/entities/create.ts index a22916f3e69f7..fc0f4c6e9a1ed 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/create.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/create.ts @@ -50,13 +50,11 @@ import { canManageEntityDefinition } from '../../lib/auth'; */ export const createEntityDefinitionRoute = createEntityManagerServerRoute({ endpoint: 'POST /internal/entities/definition', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/entities/delete.ts b/x-pack/plugins/entity_manager/server/routes/entities/delete.ts index ff5b9624dbb3c..ec5b4ada3039f 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/delete.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/delete.ts @@ -52,13 +52,11 @@ import { canDeleteEntityDefinition } from '../../lib/auth/privileges'; */ export const deleteEntityDefinitionRoute = createEntityManagerServerRoute({ endpoint: 'DELETE /internal/entities/definition/{id}', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/entities/get.ts b/x-pack/plugins/entity_manager/server/routes/entities/get.ts index f22e0890e60ad..738ed0f440643 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/get.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/get.ts @@ -50,13 +50,11 @@ import { createEntityManagerServerRoute } from '../create_entity_manager_server_ */ export const getEntityDefinitionRoute = createEntityManagerServerRoute({ endpoint: 'GET /internal/entities/definition/{id?}', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/entities/index.ts b/x-pack/plugins/entity_manager/server/routes/entities/index.ts index 539423c6a5e17..52300ab2601b6 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/index.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/index.ts @@ -10,6 +10,7 @@ import { deleteEntityDefinitionRoute } from './delete'; import { getEntityDefinitionRoute } from './get'; import { resetEntityDefinitionRoute } from './reset'; import { updateEntityDefinitionRoute } from './update'; +import { searchEntitiesRoute, searchEntitiesPreviewRoute } from '../v2/search'; export const entitiesRoutes = { ...createEntityDefinitionRoute, @@ -17,4 +18,6 @@ export const entitiesRoutes = { ...getEntityDefinitionRoute, ...resetEntityDefinitionRoute, ...updateEntityDefinitionRoute, + ...searchEntitiesRoute, + ...searchEntitiesPreviewRoute, }; diff --git a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts index ab4ba29fa1483..5da7a608aed84 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts @@ -25,13 +25,11 @@ import { stopTransforms } from '../../lib/entities/stop_transforms'; export const resetEntityDefinitionRoute = createEntityManagerServerRoute({ endpoint: 'POST /internal/entities/definition/{id}/_reset', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/entities/update.ts b/x-pack/plugins/entity_manager/server/routes/entities/update.ts index f1118028cda93..4f23486375f92 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/update.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/update.ts @@ -54,13 +54,11 @@ import { canManageEntityDefinition } from '../../lib/auth'; */ export const updateEntityDefinitionRoute = createEntityManagerServerRoute({ endpoint: 'PATCH /internal/entities/definition/{id}', - options: { - security: { - authz: { - enabled: false, - reason: - 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', - }, + security: { + authz: { + enabled: false, + reason: + 'This endpoint mainly manages Elasticsearch resources using the requesting users credentials', }, }, params: z.object({ diff --git a/x-pack/plugins/entity_manager/server/routes/v2/search.ts b/x-pack/plugins/entity_manager/server/routes/v2/search.ts new file mode 100644 index 0000000000000..0b975da748a86 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/routes/v2/search.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { z } from '@kbn/zod'; +import { createEntityManagerServerRoute } from '../create_entity_manager_server_route'; +import { entitySourceSchema } from '../../lib/queries'; +import { UnknownEntityType } from '../../lib/entities/errors/unknown_entity_type'; + +export const searchEntitiesRoute = createEntityManagerServerRoute({ + endpoint: 'POST /internal/entities/v2/_search', + params: z.object({ + body: z.object({ + type: z.string(), + metadata_fields: z.optional(z.array(z.string())).default([]), + filters: z.optional(z.array(z.string())).default([]), + start: z + .optional(z.string()) + .default(() => moment().subtract(5, 'minutes').toISOString()) + .refine((val) => moment(val).isValid(), { + message: 'start should be a date in ISO format', + }), + end: z + .optional(z.string()) + .default(() => moment().toISOString()) + .refine((val) => moment(val).isValid(), { + message: 'start should be a date in ISO format', + }), + limit: z.optional(z.number()).default(10), + }), + }), + handler: async ({ request, response, params, logger, getScopedClient }) => { + try { + const { type, start, end, limit, filters, metadata_fields: metadataFields } = params.body; + + const client = await getScopedClient({ request }); + const entities = await client.searchEntities({ + type, + filters, + metadataFields, + start, + end, + limit, + }); + + return response.ok({ body: { entities } }); + } catch (e) { + logger.error(e); + + if (e instanceof UnknownEntityType) { + return response.notFound({ body: e }); + } + + return response.customError({ body: e, statusCode: 500 }); + } + }, +}); + +export const searchEntitiesPreviewRoute = createEntityManagerServerRoute({ + endpoint: 'POST /internal/entities/v2/_search/preview', + params: z.object({ + body: z.object({ + sources: z.array(entitySourceSchema), + start: z + .optional(z.string()) + .default(() => moment().subtract(5, 'minutes').toISOString()) + .refine((val) => moment(val).isValid(), { + message: 'start should be a date in ISO format', + }), + end: z + .optional(z.string()) + .default(() => moment().toISOString()) + .refine((val) => moment(val).isValid(), { + message: 'start should be a date in ISO format', + }), + limit: z.optional(z.number()).default(10), + }), + }), + handler: async ({ request, response, params, logger, getScopedClient }) => { + const { sources, start, end, limit } = params.body; + + const client = await getScopedClient({ request }); + const entities = await client.searchEntitiesBySources({ + sources, + start, + end, + limit, + }); + + return response.ok({ body: { entities } }); + }, +}); diff --git a/x-pack/plugins/entity_manager/tsconfig.json b/x-pack/plugins/entity_manager/tsconfig.json index 34c57a27dd829..2ef8551f373fd 100644 --- a/x-pack/plugins/entity_manager/tsconfig.json +++ b/x-pack/plugins/entity_manager/tsconfig.json @@ -35,5 +35,6 @@ "@kbn/encrypted-saved-objects-plugin", "@kbn/licensing-plugin", "@kbn/core-saved-objects-server", + "@kbn/es-types", ] } diff --git a/x-pack/plugins/fields_metadata/public/hooks/use_fields_metadata/use_fields_metadata.test.ts b/x-pack/plugins/fields_metadata/public/hooks/use_fields_metadata/use_fields_metadata.test.ts index eaae7c5542c0b..8f69c11a10432 100644 --- a/x-pack/plugins/fields_metadata/public/hooks/use_fields_metadata/use_fields_metadata.test.ts +++ b/x-pack/plugins/fields_metadata/public/hooks/use_fields_metadata/use_fields_metadata.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { createUseFieldsMetadataHook, UseFieldsMetadataParams } from './use_fields_metadata'; import { FindFieldsMetadataResponsePayload } from '../../../common/latest'; @@ -46,12 +46,12 @@ describe('useFieldsMetadata', () => { it('should return the fieldsMetadata value from the API', async () => { fieldsMetadataClient.find.mockResolvedValue(mockedFieldsMetadataResponse); - const { result, waitForNextUpdate } = renderHook(() => useFieldsMetadata()); + const { result } = renderHook(() => useFieldsMetadata()); expect(result.current.loading).toBe(true); expect(result.current.fieldsMetadata).toEqual(undefined); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { fieldsMetadata, loading, error } = result.current; expect(fieldsMetadata).toEqual(fields); @@ -68,21 +68,17 @@ describe('useFieldsMetadata', () => { dataset: 'dataset_name', }; - const { waitForNextUpdate } = renderHook(() => useFieldsMetadata(params)); + renderHook(() => useFieldsMetadata(params)); - await waitForNextUpdate(); - - expect(fieldsMetadataClient.find).toHaveBeenCalledWith(params); + await waitFor(() => expect(fieldsMetadataClient.find).toHaveBeenCalledWith(params)); }); it('should return an error if the API call fails', async () => { const error = new Error('Fetch fields metadata Failed'); fieldsMetadataClient.find.mockRejectedValueOnce(error); - const { result, waitForNextUpdate } = renderHook(() => useFieldsMetadata()); - - await waitForNextUpdate(); + const { result } = renderHook(() => useFieldsMetadata()); - expect(result.current.error?.message).toMatch(error.message); + await waitFor(() => expect(result.current.error?.message).toMatch(error.message)); }); }); diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index b89577ed7c365..c3baf3b6e1755 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -38,7 +38,5 @@ export const LICENSE_FOR_SCHEDULE_UPGRADE = 'platinum'; export const DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT = 750; -export const AGENTLESS_POLICY_ID = 'agentless'; // the policy id defined here: https://github.com/elastic/project-controller/blob/main/internal/project/security/security_kibana_config.go#L86 - export const AGENT_LOG_LEVELS = ['error', 'warning', 'info', 'debug'] as const; export const DEFAULT_LOG_LEVEL = 'info' as const; diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 8ebfe005960c4..f51e85f22c526 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -25,6 +25,7 @@ export * from './message_signing_keys'; export * from './locators'; export * from './secrets'; export * from './uninstall_token'; +export * from './space_awareness'; // TODO: This is the default `index.max_result_window` ES setting, which dictates // the maximum amount of results allowed to be returned from a search. It's possible diff --git a/x-pack/plugins/fleet/common/constants/mappings.ts b/x-pack/plugins/fleet/common/constants/mappings.ts index 6499da7f86cc9..f3d2b200cac58 100644 --- a/x-pack/plugins/fleet/common/constants/mappings.ts +++ b/x-pack/plugins/fleet/common/constants/mappings.ts @@ -72,6 +72,7 @@ export const PACKAGE_POLICIES_MAPPINGS = { properties: {}, }, secret_references: { properties: { id: { type: 'keyword' } } }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, diff --git a/x-pack/plugins/fleet/common/constants/space_awareness.ts b/x-pack/plugins/fleet/common/constants/space_awareness.ts new file mode 100644 index 0000000000000..c89d0f4cddb00 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/space_awareness.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * The identifier in a saved object's `namespaces` array when it is shared to an unknown space (e.g., one that the end user is not authorized to see). + */ +export const UNKNOWN_SPACE = '?'; diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 730bcb393e987..07fd2caf0f061 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -21,7 +21,6 @@ const _allowedExperimentalValues = { kafkaOutput: true, outputSecretsStorage: true, remoteESOutput: true, - agentless: false, enableStrictKQLValidation: true, subfeaturePrivileges: false, advancedPolicySettings: true, diff --git a/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap b/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap index 7c549b030a337..f2d4067be434b 100644 --- a/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap +++ b/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap @@ -238,6 +238,7 @@ Object { "policy_ids": Array [ "policy123", ], + "supports_agentless": undefined, "vars": undefined, } `; diff --git a/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts b/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts index 14c7b3888f775..5e39f94959486 100644 --- a/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts +++ b/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts @@ -49,6 +49,7 @@ export interface SimplifiedPackagePolicy { description?: string; vars?: SimplifiedVars; inputs?: SimplifiedInputs; + supports_agentless?: boolean | null; } export interface FormattedPackagePolicy extends Omit<PackagePolicy, 'inputs' | 'vars'> { @@ -154,18 +155,19 @@ export function simplifiedPackagePolicytoNewPackagePolicy( description, inputs = {}, vars: packageLevelVars, + supports_agentless: supportsAgentless, } = data; - const packagePolicy = packageToPackagePolicy( - packageInfo, - policyId && isEmpty(policyIds) ? policyId : policyIds, - namespace, - name, - description - ); - - if (outputId) { - packagePolicy.output_id = outputId; - } + const packagePolicy = { + ...packageToPackagePolicy( + packageInfo, + policyId && isEmpty(policyIds) ? policyId : policyIds, + namespace, + name, + description + ), + supports_agentless: supportsAgentless, + output_id: outputId, + }; if (packagePolicy.package && options?.experimental_data_stream_features) { packagePolicy.package.experimental_data_stream_features = diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 841b98b239b29..fb19953a1f731 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -272,7 +272,6 @@ export interface FleetServerPolicy { export interface AgentlessApiResponse { id: string; - region_id: string; } // Definitions for agent policy outputs endpoints 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 354834d2571dc..5c98729ff6cd7 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -93,6 +93,7 @@ export interface NewPackagePolicy { [key: string]: any; }; overrides?: { inputs?: { [key: string]: any } } | null; + supports_agentless?: boolean | null; } export interface UpdatePackagePolicy extends NewPackagePolicy { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index b990e5367bb42..57e0cc4f735fe 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -220,6 +220,8 @@ export interface GetAgentStatusResponse { export interface GetAgentIncomingDataRequest { query: { agentsIds: string[]; + pkgName?: string; + pkgVersion?: string; previewData?: boolean; }; } diff --git a/x-pack/plugins/fleet/dev_docs/space_awareness.md b/x-pack/plugins/fleet/dev_docs/space_awareness.md index fc4ce3a0acd06..90371d1f8bb08 100644 --- a/x-pack/plugins/fleet/dev_docs/space_awareness.md +++ b/x-pack/plugins/fleet/dev_docs/space_awareness.md @@ -15,7 +15,7 @@ xpack.fleet.enableExperimental: ['useSpaceAwareness', 'subfeaturePrivileges'] After the feature flag is enabled you will have to do another step to opt-in for the feature, that call will migrate the current space agnostic saved objects to new space aware saved objects. ```shell -curl -u elastic:changeme -XPOST "http://localhost:5601/internal/fleet/enable_space_awareness" -H "kbn-xsrf: reporting" -H 'elastic-api-version: 1' +curl -u elastic:changeme -XPOST "http://localhost:5601/internal/fleet/enable_space_awareness" -H "kbn-xsrf: reporting" -H 'elastic-api-version: 1' -H 'x-elastic-internal-origin: 1' ``` ## Space aware entities in Fleet diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx index e404b30a37ced..61846bdbd7e24 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx @@ -18,6 +18,8 @@ import type { AgentPolicy, NewAgentPolicy } from '../../../../../../../common/ty import { useLicense } from '../../../../../../hooks/use_license'; +import { useFleetStatus } from '../../../../hooks'; + import type { LicenseService } from '../../../../../../../common/services'; import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services'; @@ -26,8 +28,13 @@ import type { ValidationResults } from '../agent_policy_validation'; import { AgentPolicyAdvancedOptionsContent } from '.'; jest.mock('../../../../../../hooks/use_license'); +jest.mock('../../../../hooks', () => ({ + ...jest.requireActual('../../../../hooks'), + useFleetStatus: jest.fn(), +})); const mockedUseLicence = useLicense as jest.MockedFunction<typeof useLicense>; +const mockedUseFleetStatus = useFleetStatus as jest.MockedFunction<typeof useFleetStatus>; describe('Agent policy advanced options content', () => { let testRender: TestRenderer; @@ -40,6 +47,10 @@ describe('Agent policy advanced options content', () => { hasAtLeast: () => true, isPlatinum: () => true, } as unknown as LicenseService); + const useSpaceAwareness = () => + mockedUseFleetStatus.mockReturnValue({ + isSpaceAwarenessEnabled: true, + } as any); const render = ({ isProtected = false, @@ -47,6 +58,7 @@ describe('Agent policy advanced options content', () => { policyId = 'agent-policy-1', newAgentPolicy = false, packagePolicy = [createPackagePolicyMock()], + spaceIds = ['default'], } = {}) => { if (newAgentPolicy) { mockAgentPolicy = generateNewAgentPolicyWithDefaults(); @@ -56,6 +68,7 @@ describe('Agent policy advanced options content', () => { package_policies: packagePolicy, id: policyId, is_managed: isManaged, + space_ids: spaceIds, }; } @@ -72,6 +85,7 @@ describe('Agent policy advanced options content', () => { }; beforeEach(() => { + mockedUseFleetStatus.mockReturnValue({} as any); testRender = createFleetTestRendererMock(); }); afterEach(() => { @@ -173,4 +187,39 @@ describe('Agent policy advanced options content', () => { expect(renderResult.queryByText('This policy has no custom fields')).toBeInTheDocument(); }); }); + + describe('Space selector', () => { + beforeEach(() => { + usePlatinumLicense(); + }); + + describe('when space awareness is disabled', () => { + it('should not be rendered', () => { + render(); + expect(renderResult.queryByTestId('spaceSelectorInput')).not.toBeInTheDocument(); + }); + }); + + describe('when space awareness is enabled', () => { + beforeEach(() => { + useSpaceAwareness(); + }); + + describe('when the user has access to all policy spaces', () => { + it('should render the space selection input with the Create space link', () => { + render(); + expect(renderResult.queryByTestId('spaceSelectorInput')).toBeInTheDocument(); + expect(renderResult.queryByTestId('spaceSelectorInputLink')).toBeInTheDocument(); + }); + }); + + describe('when the user does not have access to all policy spaces', () => { + it('should render the space selection input without the Create space link', () => { + render({ spaceIds: ['default', '?'] }); + expect(renderResult.queryByTestId('spaceSelectorInput')).toBeInTheDocument(); + expect(renderResult.queryByTestId('spaceSelectorInputLink')).not.toBeInTheDocument(); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 305148584f545..b0889f825727f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -34,6 +34,7 @@ import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, dataTypes, DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT, + UNKNOWN_SPACE, } from '../../../../../../../common/constants'; import type { NewAgentPolicy, AgentPolicy } from '../../../../types'; import { @@ -127,7 +128,12 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> = const isManagedorAgentlessPolicy = agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true; - const agentPolicyFormContect = useAgentPolicyFormContext(); + const userHasAccessToAllPolicySpaces = useMemo( + () => 'space_ids' in agentPolicy && !agentPolicy.space_ids?.includes(UNKNOWN_SPACE), + [agentPolicy] + ); + + const agentPolicyFormContext = useAgentPolicyFormContext(); const AgentTamperProtectionSectionContent = useMemo( () => ( @@ -309,13 +315,14 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> = description={ <FormattedMessage id="xpack.fleet.agentPolicyForm.spaceDescription" - defaultMessage="Select one or more spaces for this policy or create a new one. {link}" + defaultMessage="Select one or more spaces for this policy or create a new one. {link}{tooltip}" values={{ - link: ( + link: userHasAccessToAllPolicySpaces && ( <EuiLink target="_blank" href={getAbsolutePath('/app/management/kibana/spaces/create')} external + data-test-subj="spaceSelectorInputLink" > <FormattedMessage id="xpack.fleet.agentPolicyForm.createSpaceLink" @@ -323,18 +330,30 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> = /> </EuiLink> ), + tooltip: !userHasAccessToAllPolicySpaces && ( + <EuiIconTip + type="iInCircle" + color="subdued" + content={i18n.translate('xpack.fleet.agentPolicyForm.spaceTooltip', { + defaultMessage: 'Access to all policy spaces is required for edit.', + })} + /> + ), }} /> } + data-test-subj="spaceSelectorInput" > <SpaceSelector - isDisabled={disabled || agentPolicy.is_managed === true} + isDisabled={ + disabled || agentPolicy.is_managed === true || !userHasAccessToAllPolicySpaces + } value={ 'space_ids' in agentPolicy && agentPolicy.space_ids - ? agentPolicy.space_ids + ? agentPolicy.space_ids.filter((id) => id !== UNKNOWN_SPACE) : [spaceId || 'default'] } - setInvalidSpaceError={agentPolicyFormContect?.setInvalidSpaceError} + setInvalidSpaceError={agentPolicyFormContext?.setInvalidSpaceError} onChange={(newValue) => { if (newValue.length === 0) { return; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx index 5accdf37e95e7..0222e8a238b9d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx @@ -360,11 +360,8 @@ describe('PackagePolicyInputPanel', () => { beforeEach(() => { useAgentlessMock.mockReturnValue({ isAgentlessEnabled: true, - isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessApiEnabled: true, - isDefaultAgentlessPolicyEnabled: false, }); }); @@ -395,11 +392,8 @@ describe('PackagePolicyInputPanel', () => { beforeEach(() => { useAgentlessMock.mockReturnValue({ isAgentlessEnabled: false, - isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessApiEnabled: true, - isDefaultAgentlessPolicyEnabled: false, }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx index 1642abe05d72b..d6d4c3abd63f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx @@ -131,11 +131,11 @@ export const ConfirmIncomingDataWithPreview: React.FunctionComponent<Props> = ({ setAgentDataConfirmed, troubleshootLink, }) => { - const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData( + const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData({ agentIds, - true, - MAX_AGENT_DATA_PREVIEW_COUNT - ); + previewData: true, + stopPollingAfterPreviewLength: MAX_AGENT_DATA_PREVIEW_COUNT, + }); const { enrolledAgents, numAgentsWithData } = useGetAgentIncomingData(incomingData, packageInfo); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(packageInfo?.name); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 3dbe300b119bd..4c4c7b311cb06 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -178,8 +178,7 @@ export function useOnSubmit({ const [hasAgentPolicyError, setHasAgentPolicyError] = useState<boolean>(false); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - const { isAgentlessIntegration, isAgentlessAgentPolicy, isAgentlessPackagePolicy } = - useAgentless(); + const { isAgentlessIntegration, isAgentlessAgentPolicy } = useAgentless(); // Update agent policy method const updateAgentPolicies = useCallback( @@ -316,9 +315,7 @@ export function useOnSubmit({ (agentCount !== 0 || (agentPolicies.length === 0 && selectedPolicyTab !== SelectedPolicyTab.NEW)) && !( - isAgentlessIntegration(packageInfo) || - isAgentlessPackagePolicy(packagePolicy) || - isAgentlessAgentPolicy(overrideCreatedAgentPolicy) + isAgentlessIntegration(packageInfo) || isAgentlessAgentPolicy(overrideCreatedAgentPolicy) ) && formState !== 'CONFIRM' ) { @@ -365,9 +362,7 @@ export function useOnSubmit({ : packagePolicy.policy_ids; const shouldForceInstallOnAgentless = - isAgentlessAgentPolicy(createdPolicy) || - isAgentlessIntegration(packageInfo) || - isAgentlessPackagePolicy(packagePolicy); + isAgentlessAgentPolicy(createdPolicy) || isAgentlessIntegration(packageInfo); const forceInstall = force || shouldForceInstallOnAgentless; @@ -390,8 +385,7 @@ export function useOnSubmit({ const hasGoogleCloudShell = data?.item ? getCloudShellUrlFromPackagePolicy(data.item) : false; // Check if agentless is configured in ESS and Serverless until Agentless API migrates to Serverless - const isAgentlessConfigured = - isAgentlessAgentPolicy(createdPolicy) || (data && isAgentlessPackagePolicy(data.item)); + const isAgentlessConfigured = isAgentlessAgentPolicy(createdPolicy); // Removing this code will disabled the Save and Continue button. We need code below update form state and trigger correct modal depending on agent count if (hasFleetAddAgentsPrivileges && !isAgentlessConfigured) { @@ -479,7 +473,6 @@ export function useOnSubmit({ selectedPolicyTab, packagePolicy, isAgentlessAgentPolicy, - isAgentlessPackagePolicy, hasFleetAddAgentsPrivileges, withSysMonitoring, newAgentPolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index 0953f2367d05c..1564d934b960e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -11,8 +11,7 @@ import { createPackagePolicyMock } from '../../../../../../../../common/mocks'; import type { RegistryPolicyTemplate, PackageInfo } from '../../../../../../../../common/types'; import { SetupTechnology } from '../../../../../../../../common/types'; -import { ExperimentalFeaturesService } from '../../../../../services'; -import { sendGetOneAgentPolicy, useStartServices, useConfig } from '../../../../../hooks'; +import { useStartServices, useConfig } from '../../../../../hooks'; import { SelectedPolicyTab } from '../../components'; import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy'; @@ -30,12 +29,7 @@ jest.mock('../../../../../../../../common/services/generate_new_agent_policy'); type MockFn = jest.MockedFunction<any>; describe('useAgentless', () => { - const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); - beforeEach(() => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: false, - } as any); (useConfig as MockFn).mockReturnValue({ agentless: undefined, } as any); @@ -48,33 +42,25 @@ describe('useAgentless', () => { jest.clearAllMocks(); }); - it('should not return isAgentless when agentless is not enabled', () => { + it('should return isAgentlessEnabled as falsy when agentless is not enabled', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessApiEnabled).toBeFalsy(); - expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled as falsy if agentless.enabled is true and experimental feature agentless is truthy without cloud or serverless', () => { + it('should return isAgentlessEnabled as falsy if agentless.enabled true without cloud or serverless', () => { (useConfig as MockFn).mockReturnValue({ agentless: { enabled: true, }, } as any); - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: false, - } as any); - const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessApiEnabled).toBeFalsy(); - expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isAgentlessApiEnabled as truthy with isCloudEnabled', () => { + it('should return isAgentlessEnabled as truthy with isCloudEnabled', () => { (useConfig as MockFn).mockReturnValue({ agentless: { enabled: true, @@ -91,14 +77,9 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessApiEnabled).toBeTruthy(); - expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isDefaultAgentlessPolicyEnabled as truthy with isServerlessEnabled and experimental feature agentless is truthy', () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: true, - } as any); + it('should return isAgentlessEnabled truthy with isServerlessEnabled', () => { (useStartServices as MockFn).mockReturnValue({ cloud: { isServerlessEnabled: true, @@ -106,18 +87,18 @@ describe('useAgentless', () => { }, }); + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + }, + } as any); + const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessApiEnabled).toBeFalsy(); - expect(result.current.isDefaultAgentlessPolicyEnabled).toBeTruthy(); }); - it('should return isAgentlessEnabled as falsy and isDefaultAgentlessPolicyEnabled as falsy with isServerlessEnabled and experimental feature agentless is falsy', () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: false, - } as any); - + it('should return isAgentlessEnabled as falsy with isServerlessEnabled and without agentless config', () => { (useStartServices as MockFn).mockReturnValue({ cloud: { isServerlessEnabled: true, @@ -128,14 +109,13 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessApiEnabled).toBeFalsy(); - expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); }); describe('useSetupTechnology', () => { const setNewAgentPolicy = jest.fn(); const updateAgentPoliciesMock = jest.fn(); + const updatePackagePolicyMock = jest.fn(); const setSelectedPolicyTabMock = jest.fn(); const newAgentPolicyMock = { name: 'mock_new_agent_policy', @@ -178,20 +158,10 @@ describe('useSetupTechnology', () => { const packagePolicyMock = createPackagePolicyMock(); - const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); - beforeEach(() => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: true, - } as any); (useConfig as MockFn).mockReturnValue({ agentless: undefined, } as any); - (sendGetOneAgentPolicy as MockFn).mockResolvedValue({ - data: { - item: { id: 'agentless-policy-id' }, - }, - }); (useStartServices as MockFn).mockReturnValue({ cloud: { isServerlessEnabled: true, @@ -207,108 +177,20 @@ describe('useSetupTechnology', () => { }); it('should initialize with default values when agentless is disabled', () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: false, - } as any); - - const { result } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, - }) - ); - - expect(sendGetOneAgentPolicy).not.toHaveBeenCalled(); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - }); - - it('should set the default selected setup technology to agent-based when creating a non agentless-only package policy', async () => { - (useConfig as MockFn).mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'https://agentless.api.url', - }, - }, - } as any); - (useStartServices as MockFn).mockReturnValue({ - cloud: { - isCloudEnabled: true, - }, - }); - const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, - packageInfo: packageInfoMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); }); - it('should set the default selected setup technology to agentless when creating an agentless-only package policy', async () => { - (useConfig as MockFn).mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'https://agentless.api.url', - }, - }, - } as any); - (useStartServices as MockFn).mockReturnValue({ - cloud: { - isCloudEnabled: true, - }, - }); - const agentlessOnlyPackageInfoMock = { - policy_templates: [ - { - deployment_modes: { - default: { enabled: false }, - agentless: { enabled: true }, - }, - }, - ], - } as PackageInfo; - - const { result } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packageInfo: agentlessOnlyPackageInfoMock, - packagePolicy: packagePolicyMock, - }) - ); - - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); - }); - - it('should fetch agentless policy if agentless feature is enabled and isServerless is true', async () => { - renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, - }) - ); - - await waitFor(() => { - expect(sendGetOneAgentPolicy).toHaveBeenCalled(); - }); - }); - it('should set agentless setup technology if agent policy supports agentless in edit page', async () => { (useConfig as MockFn).mockReturnValue({ agentless: { @@ -332,13 +214,14 @@ describe('useSetupTechnology', () => { packagePolicy: packagePolicyMock, isEditPage: true, agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + updatePackagePolicy: updatePackagePolicyMock, }) ); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); }); - it('should create agentless policy if agentless feature is enabled and isCloud is true and agentless.api.url', async () => { + it('should create agentless policy if isCloud and agentless.enabled', async () => { (useConfig as MockFn).mockReturnValue({ agentless: { enabled: true, @@ -359,6 +242,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -368,6 +252,7 @@ describe('useSetupTechnology', () => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); await waitFor(() => { + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', @@ -398,6 +283,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }; const { result, rerender } = renderHook((props = initialProps) => useSetupTechnology(props), { @@ -411,6 +297,7 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); expect(setNewAgentPolicy).toHaveBeenCalledWith({ inactivity_timeout: 3600, name: 'Agentless policy for endpoint-1', @@ -426,9 +313,11 @@ describe('useSetupTechnology', () => { ...packagePolicyMock, name: 'endpoint-2', }, + updatePackagePolicy: updatePackagePolicyMock, }); await waitFor(() => { + expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-2', inactivity_timeout: 3600, @@ -437,7 +326,7 @@ describe('useSetupTechnology', () => { }); }); - it('should not create agentless policy if agentless feature is enabled and isCloud is true and agentless.api.url is not defined', async () => { + it('should not create agentless policy isCloud is true and agentless.api.url is not defined', async () => { (useConfig as MockFn).mockReturnValue({} as any); (useStartServices as MockFn).mockReturnValue({ cloud: { @@ -452,6 +341,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -464,10 +354,18 @@ describe('useSetupTechnology', () => { await waitFor(() => expect(setNewAgentPolicy).toHaveBeenCalledTimes(0)); }); - it('should not fetch agentless policy if agentless is enabled but serverless is disabled', async () => { + it('should update new agent policy and selected policy tab when setup technology is agent-based', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); (useStartServices as MockFn).mockReturnValue({ cloud: { - isServerlessEnabled: false, + isCloudEnabled: true, }, }); @@ -478,48 +376,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, - }) - ); - - expect(sendGetOneAgentPolicy).not.toHaveBeenCalled(); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - }); - - it('should update agent policy and selected policy tab when setup technology is agentless', async () => { - const { result } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, - }) - ); - - act(() => { - result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); - }); - - await waitFor(() => { - expect(updateAgentPoliciesMock).toHaveBeenCalledWith([ - { - inactivity_timeout: 3600, - name: 'Agentless policy for endpoint-1', - supports_agentless: true, - }, - ]); - expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.EXISTING); - }); - }); - - it('should update new agent policy and selected policy tab when setup technology is agent-based', async () => { - const { result } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -530,12 +387,14 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); act(() => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED); }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false }); await waitFor(() => { expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); @@ -543,30 +402,6 @@ describe('useSetupTechnology', () => { }); }); - it('should not update agent policy and selected policy tab when agentless is disabled', async () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - agentless: false, - } as any); - - const { result } = renderHook(() => - useSetupTechnology({ - setNewAgentPolicy, - newAgentPolicy: newAgentPolicyMock, - updateAgentPolicies: updateAgentPoliciesMock, - setSelectedPolicyTab: setSelectedPolicyTabMock, - packagePolicy: packagePolicyMock, - }) - ); - - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - - act(() => { - result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); - }); - - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - }); - it('should not update agent policy and selected policy tab when setup technology matches the current one ', async () => { const { result } = renderHook(() => useSetupTechnology({ @@ -575,6 +410,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -588,11 +424,25 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); + expect(updatePackagePolicyMock).not.toHaveBeenCalled(); expect(setNewAgentPolicy).not.toHaveBeenCalled(); expect(setSelectedPolicyTabMock).not.toHaveBeenCalled(); }); it('should revert the agent policy name to the original value when switching from agentless back to agent-based', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isServerlessEnabled: true, + }, + }); const { result } = renderHook(() => useSetupTechnology({ setNewAgentPolicy, @@ -600,6 +450,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -609,7 +460,9 @@ describe('useSetupTechnology', () => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + await waitFor(() => + expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS) + ); await waitFor(() => { expect(setNewAgentPolicy).toHaveBeenCalledWith({ @@ -617,14 +470,18 @@ describe('useSetupTechnology', () => { supports_agentless: true, inactivity_timeout: 3600, }); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); }); act(() => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED); }); - expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); + await waitFor(() => { + expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); + expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false }); + }); }); it('should have global_data_tags with the integration team when creating agentless policy with global_data_tags', async () => { @@ -650,6 +507,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -694,6 +552,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -743,6 +602,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -788,6 +648,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -834,6 +695,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 6bd3288af2e0d..85f5cbdc5fae1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -8,7 +8,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useConfig } from '../../../../../hooks'; -import { ExperimentalFeaturesService } from '../../../../../services'; import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy'; import type { AgentPolicy, @@ -17,10 +16,9 @@ import type { PackageInfo, } from '../../../../../types'; import { SetupTechnology } from '../../../../../types'; -import { sendGetOneAgentPolicy, useStartServices } from '../../../../../hooks'; +import { useStartServices } from '../../../../../hooks'; import { SelectedPolicyTab } from '../../components'; import { - AGENTLESS_POLICY_ID, AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, AGENTLESS_GLOBAL_TAG_NAME_DIVISION, AGENTLESS_GLOBAL_TAG_NAME_TEAM, @@ -33,23 +31,15 @@ import { export const useAgentless = () => { const config = useConfig(); - const { agentless: agentlessExperimentalFeatureEnabled } = ExperimentalFeaturesService.get(); const { cloud } = useStartServices(); const isServerless = !!cloud?.isServerlessEnabled; const isCloud = !!cloud?.isCloudEnabled; - const isAgentlessApiEnabled = (isCloud || isServerless) && config.agentless?.enabled; - const isDefaultAgentlessPolicyEnabled = - !isAgentlessApiEnabled && isServerless && agentlessExperimentalFeatureEnabled; - - const isAgentlessEnabled = isAgentlessApiEnabled || isDefaultAgentlessPolicyEnabled; + const isAgentlessEnabled = (isCloud || isServerless) && config.agentless?.enabled === true; const isAgentlessAgentPolicy = (agentPolicy: AgentPolicy | undefined) => { if (!agentPolicy) return false; - return ( - isAgentlessEnabled && - (agentPolicy?.id === AGENTLESS_POLICY_ID || !!agentPolicy?.supports_agentless) - ); + return isAgentlessEnabled && !!agentPolicy?.supports_agentless; }; // When an integration has at least a policy template enabled for agentless @@ -60,17 +50,10 @@ export const useAgentless = () => { return false; }; - // TODO: remove this check when CSPM implements the above flag and rely only on `isAgentlessIntegration` - const isAgentlessPackagePolicy = (packagePolicy: NewPackagePolicy) => { - return isAgentlessEnabled && packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID); - }; return { - isAgentlessApiEnabled, - isDefaultAgentlessPolicyEnabled, isAgentlessEnabled, isAgentlessAgentPolicy, isAgentlessIntegration, - isAgentlessPackagePolicy, }; }; @@ -78,6 +61,7 @@ export function useSetupTechnology({ setNewAgentPolicy, newAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packageInfo, packagePolicy, @@ -87,14 +71,14 @@ export function useSetupTechnology({ setNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; updateAgentPolicies: (policies: AgentPolicy[]) => void; + updatePackagePolicy: (policy: Partial<NewPackagePolicy>) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; packageInfo?: PackageInfo; packagePolicy: NewPackagePolicy; isEditPage?: boolean; agentPolicies?: AgentPolicy[]; }) { - const { isAgentlessEnabled, isAgentlessApiEnabled, isDefaultAgentlessPolicyEnabled } = - useAgentless(); + const { isAgentlessEnabled } = useAgentless(); // this is a placeholder for the new agent-BASED policy that will be used when the user switches from agentless to agent-based and back const newAgentBasedPolicy = useRef<NewAgentPolicy>(newAgentPolicy); @@ -119,7 +103,7 @@ export function useSetupTechnology({ setSelectedSetupTechnology(SetupTechnology.AGENTLESS); return; } - if (isAgentlessApiEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { + if (isAgentlessEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { const nextNewAgentlessPolicy = { ...newAgentlessPolicy, name: getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicy.name), @@ -130,42 +114,35 @@ export function useSetupTechnology({ updateAgentPolicies([nextNewAgentlessPolicy] as AgentPolicy[]); } } + if ( + selectedSetupTechnology === SetupTechnology.AGENTLESS && + !packagePolicy.supports_agentless + ) { + updatePackagePolicy({ + supports_agentless: true, + }); + } else if ( + selectedSetupTechnology !== SetupTechnology.AGENTLESS && + packagePolicy.supports_agentless + ) { + updatePackagePolicy({ + supports_agentless: false, + }); + } }, [ - isAgentlessApiEnabled, + isAgentlessEnabled, isEditPage, newAgentlessPolicy, packagePolicy.name, + packagePolicy.supports_agentless, selectedSetupTechnology, updateAgentPolicies, setNewAgentPolicy, agentPolicies, setSelectedSetupTechnology, + updatePackagePolicy, ]); - // tech debt: remove this useEffect when Serverless uses the Agentless API - // https://github.com/elastic/security-team/issues/9781 - useEffect(() => { - const fetchAgentlessPolicy = async () => { - const { data, error } = await sendGetOneAgentPolicy(AGENTLESS_POLICY_ID); - const isAgentlessAvailable = !error && data && data.item; - - if (isAgentlessAvailable) { - setNewAgentlessPolicy(data.item); - } - }; - - if (isDefaultAgentlessPolicyEnabled) { - fetchAgentlessPolicy(); - } - }, [isDefaultAgentlessPolicyEnabled]); - - useEffect(() => { - if (isEditPage) { - return; - } - setSelectedSetupTechnology(defaultSetupTechnology); - }, [packageInfo, defaultSetupTechnology, isEditPage]); - const handleSetupTechnologyChange = useCallback( (setupTechnology: SetupTechnology, policyTemplateName?: string) => { if (!isAgentlessEnabled || setupTechnology === selectedSetupTechnology) { @@ -173,7 +150,7 @@ export function useSetupTechnology({ } if (setupTechnology === SetupTechnology.AGENTLESS) { - if (isAgentlessApiEnabled) { + if (isAgentlessEnabled) { const agentlessPolicy = { ...newAgentlessPolicy, ...getAdditionalAgentlessPolicyInfo(policyTemplateName, packageInfo), @@ -184,18 +161,17 @@ export function useSetupTechnology({ setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([agentlessPolicy] as AgentPolicy[]); } - // tech debt: remove this when Serverless uses the Agentless API - // https://github.com/elastic/security-team/issues/9781 - if (isDefaultAgentlessPolicyEnabled) { - setNewAgentPolicy(newAgentlessPolicy as AgentPolicy); - updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); - setSelectedPolicyTab(SelectedPolicyTab.EXISTING); - } + updatePackagePolicy({ + supports_agentless: true, + }); } else if (setupTechnology === SetupTechnology.AGENT_BASED) { setNewAgentPolicy({ ...newAgentBasedPolicy.current, supports_agentless: false, }); + updatePackagePolicy({ + supports_agentless: false, + }); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]); } @@ -204,13 +180,12 @@ export function useSetupTechnology({ [ isAgentlessEnabled, selectedSetupTechnology, - isAgentlessApiEnabled, - isDefaultAgentlessPolicyEnabled, + updatePackagePolicy, setNewAgentPolicy, newAgentlessPolicy, + packageInfo, setSelectedPolicyTab, updateAgentPolicies, - packageInfo, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index e3b1959475649..a79db9ea1edda 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -13,17 +13,12 @@ import type { MockedFleetStartServices, TestRenderer } from '../../../../../../m import { createFleetTestRendererMock } from '../../../../../../mock'; import { FLEET_ROUTING_PATHS, pagePathGetters, PLUGIN_ID } from '../../../../constants'; import type { CreatePackagePolicyRouteState } from '../../../../types'; - -import { ExperimentalFeaturesService } from '../../../../../../services'; - import { sendCreatePackagePolicy, sendCreateAgentPolicy, sendGetAgentStatus, - sendGetOneAgentPolicy, useIntraAppState, useStartServices, - useGetAgentPolicies, useGetPackageInfoByKeyQuery, useConfig, } from '../../../../hooks'; @@ -138,10 +133,6 @@ jest.mock('react-router-dom', () => ({ }), })); -import { AGENTLESS_POLICY_ID } from '../../../../../../../common/constants'; - -import { useAllNonManagedAgentPolicies } from '../components/steps/components/use_policies'; - import { CreatePackagePolicySinglePage } from '.'; import { SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ } from './components/setup_technology_selector'; @@ -692,108 +683,6 @@ describe('When on the package policy create page', () => { }); }); - describe('With agentless policy and Serverless available', () => { - beforeEach(async () => { - (useStartServices as jest.MockedFunction<any>).mockReturnValue({ - ...useStartServices(), - cloud: { - ...useStartServices().cloud, - isServerlessEnabled: true, - }, - }); - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: true } as any); - (useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue( - getMockPackageInfo({ requiresRoot: false, dataStreamRequiresRoot: false }) - ); - - (sendGetOneAgentPolicy as jest.MockedFunction<any>).mockResolvedValue({ - data: { item: { id: AGENTLESS_POLICY_ID, name: 'Agentless CSPM', namespace: 'default' } }, - }); - (useGetAgentPolicies as jest.MockedFunction<any>).mockReturnValue({ - data: { - items: [{ id: AGENTLESS_POLICY_ID, name: 'Agentless CSPM', namespace: 'default' }], - }, - error: undefined, - isLoading: false, - resendRequest: jest.fn(), - }); - (useAllNonManagedAgentPolicies as jest.MockedFunction<any>).mockReturnValue([ - { id: AGENTLESS_POLICY_ID, name: 'Agentless CSPM', namespace: 'default' }, - ]); - - await act(async () => { - render(); - }); - }); - - test('should not force create package policy when not in serverless', async () => { - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: false } as any); - (useStartServices as jest.MockedFunction<any>).mockReturnValue({ - ...useStartServices(), - cloud: { - ...useStartServices().cloud, - isServerlessEnabled: false, - }, - }); - await act(async () => { - fireEvent.click(renderResult.getByText('Existing hosts')!); - }); - - await act(async () => { - fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); - }); - - expect(sendCreateAgentPolicy as jest.MockedFunction<any>).not.toHaveBeenCalled(); - expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalledWith({ - ...newPackagePolicy, - force: false, - policy_ids: [AGENTLESS_POLICY_ID], - }); - - await waitFor(() => { - expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); - }); - }); - - test('should force create package policy', async () => { - await act(async () => { - fireEvent.click(renderResult.getByText('Existing hosts')!); - }); - - await act(async () => { - fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); - }); - - expect(sendCreateAgentPolicy as jest.MockedFunction<any>).not.toHaveBeenCalled(); - expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalledWith({ - ...newPackagePolicy, - force: true, - policy_ids: [AGENTLESS_POLICY_ID], - }); - - await waitFor(() => { - expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); - }); - }); - - test('should not show confirmation modal', async () => { - (sendGetAgentStatus as jest.MockedFunction<any>).mockResolvedValueOnce({ - data: { results: { active: 1 } }, - }); - - await act(async () => { - fireEvent.click(renderResult.getByText('Existing hosts')!); - }); - - await act(async () => { - fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); - }); - - expect(sendCreateAgentPolicy as jest.MockedFunction<any>).not.toHaveBeenCalled(); - expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalled(); - }); - }); - describe('With agentless Cloud available', () => { beforeEach(async () => { (useConfig as jest.MockedFunction<any>).mockReturnValue({ @@ -819,10 +708,6 @@ describe('When on the package policy create page', () => { }, }); - (sendCreatePackagePolicy as jest.MockedFunction<any>).mockResolvedValue({ - data: { item: { id: 'policy-1', inputs: [], policy_ids: ['agentless-policy-1'] } }, - }); - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: true } as any); (useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue( getMockPackageInfo({ requiresRoot: false, @@ -840,9 +725,6 @@ describe('When on the package policy create page', () => { fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); }); - // tech debt: this should be converted to use MSW to mock the API calls - // https://github.com/elastic/security-team/issues/9816 - expect(sendGetOneAgentPolicy).not.toHaveBeenCalled(); expect(sendCreateAgentPolicy).toHaveBeenCalledWith( expect.objectContaining({ monitoring_enabled: ['logs', 'metrics'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 93631f63d0e04..3941c92a76c39 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -355,6 +355,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ newAgentPolicy, setNewAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packageInfo, packagePolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 9de7a3f22adff..e0ea955cf5afe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -39,13 +39,13 @@ import { import { DevtoolsRequestFlyoutButton } from '../../../../../components'; import { ExperimentalFeaturesService } from '../../../../../services'; import { generateUpdateAgentPolicyDevToolsRequest } from '../../../services'; +import { UNKNOWN_SPACE } from '../../../../../../../../common/constants'; -const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) => - pick(agentPolicy, [ +const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) => { + const partialPolicy = pick(agentPolicy, [ 'name', 'description', 'namespace', - 'space_ids', 'monitoring_enabled', 'unenroll_timeout', 'inactivity_timeout', @@ -61,6 +61,13 @@ const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) => 'monitoring_http', 'monitoring_diagnostics', ]); + return { + ...partialPolicy, + ...(!agentPolicy.space_ids?.includes(UNKNOWN_SPACE) && { + space_ids: agentPolicy.space_ids, + }), + }; +}; const FormWrapper = styled.div` max-width: 1200px; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx index 1f2bdecf9e5ad..56f6a747dd574 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx @@ -134,6 +134,7 @@ export function usePackagePolicySteps({ newAgentPolicy, setNewAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packagePolicy, isEditPage: true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx index 79908819ff863..03f7b2b33a83c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx @@ -21,6 +21,7 @@ jest.mock('../../../../../../hooks/use_fleet_status', () => ({ FleetStatusProvider: (props: any) => { return props.children; }, + useFleetStatus: jest.fn().mockReturnValue({ spaceId: 'default' }), })); jest.mock('../../../../../../hooks/use_request/epm'); @@ -30,7 +31,7 @@ jest.mock('../../../../../../hooks/use_locator', () => { useDashboardLocator: jest.fn().mockImplementation(() => { return { id: 'DASHBOARD_APP_LOCATOR', - getRedirectUrl: jest.fn().mockResolvedValue('app/dashboards#/view/elastic_agent-a0001'), + getRedirectUrl: jest.fn().mockReturnValue('app/dashboards#/view/elastic_agent-a0001'), }; }), }; @@ -43,6 +44,10 @@ describe('AgentDashboardLink', () => { data: { item: { status: 'installed', + installationInfo: { + install_status: 'installed', + installed_kibana_space_id: 'default', + }, }, }, } as ReturnType<typeof useGetPackageInfoByKeyQuery>); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx index 6832f81961ddb..c6a7c6b1a7d43 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx @@ -10,21 +10,49 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; -import { useGetPackageInfoByKeyQuery, useLink, useDashboardLocator } from '../../../../hooks'; +import type { GetInfoResponse } from '../../../../../../../common/types'; +import { + useGetPackageInfoByKeyQuery, + useLink, + useDashboardLocator, + useFleetStatus, +} from '../../../../hooks'; import type { Agent, AgentPolicy } from '../../../../types'; import { FLEET_ELASTIC_AGENT_PACKAGE, DASHBOARD_LOCATORS_IDS, } from '../../../../../../../common/constants'; +import { getDashboardIdForSpace } from '../../services/dashboard_helpers'; + +function isKibanaAssetsInstalledInSpace(spaceId: string | undefined, res?: GetInfoResponse) { + if (res?.item?.status !== 'installed') { + return false; + } + + const installationInfo = res.item.installationInfo; + + if (!installationInfo || installationInfo.install_status !== 'installed') { + return false; + } + return ( + installationInfo.installed_kibana_space_id === spaceId || + (spaceId && installationInfo.additional_spaces_installed_kibana?.[spaceId]) + ); +} function useAgentDashboardLink(agent: Agent) { const { isLoading, data } = useGetPackageInfoByKeyQuery(FLEET_ELASTIC_AGENT_PACKAGE); + const { spaceId } = useFleetStatus(); - const isInstalled = data?.item.status === 'installed'; + const isInstalled = isKibanaAssetsInstalledInSpace(spaceId, data); const dashboardLocator = useDashboardLocator(); const link = dashboardLocator?.getRedirectUrl({ - dashboardId: DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS, + dashboardId: getDashboardIdForSpace( + spaceId, + data, + DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS + ), query: { language: 'kuery', query: `elastic_agent.id:${agent.id}`, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx index db14401edfadb..84895a021df35 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx @@ -97,114 +97,122 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ agent: Agent; agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; + linkToLogs: boolean; 'data-test-subj'?: string; -}> = memo(({ agent, agentPolicy, packagePolicy, 'data-test-subj': dataTestSubj }) => { - const { getHref } = useLink(); - const theme = useEuiTheme(); +}> = memo( + ({ agent, agentPolicy, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => { + const { getHref } = useLink(); + const theme = useEuiTheme(); - const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] = - useState(false); + const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] = + useState(false); - const policyResponseExtensionView = useUIExtension( - packagePolicy.package?.name ?? '', - 'package-policy-response' - ); - - const policyResponseExtensionViewWrapper = useMemo(() => { - return ( - policyResponseExtensionView && ( - <ExtensionWrapper> - <policyResponseExtensionView.Component - agent={agent} - onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse} - /> - </ExtensionWrapper> - ) - ); - }, [agent, policyResponseExtensionView]); - - const packageErrors = useMemo(() => { - if (!agent.components) { - return []; - } - return getInputUnitsByPackage(agent.components, packagePolicy).filter( - (u) => u.status === 'DEGRADED' || u.status === 'FAILED' + const policyResponseExtensionView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-policy-response' ); - }, [agent.components, packagePolicy]); - const showNeedsAttentionBadge = isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length; + const policyResponseExtensionViewWrapper = useMemo(() => { + return ( + policyResponseExtensionView && ( + <ExtensionWrapper> + <policyResponseExtensionView.Component + agent={agent} + onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse} + /> + </ExtensionWrapper> + ) + ); + }, [agent, policyResponseExtensionView]); + + const packageErrors = useMemo(() => { + if (!agent.components) { + return []; + } + return getInputUnitsByPackage(agent.components, packagePolicy).filter( + (u) => u.status === 'DEGRADED' || u.status === 'FAILED' + ); + }, [agent.components, packagePolicy]); - const genericErrorsListExtensionView = useUIExtension( - packagePolicy.package?.name ?? '', - 'package-generic-errors-list' - ); + const showNeedsAttentionBadge = + isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length; - const genericErrorsListExtensionViewWrapper = useMemo(() => { - return ( - genericErrorsListExtensionView && ( - <ExtensionWrapper> - <genericErrorsListExtensionView.Component packageErrors={packageErrors} /> - </ExtensionWrapper> - ) + const genericErrorsListExtensionView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-generic-errors-list' ); - }, [packageErrors, genericErrorsListExtensionView]); - return ( - <CollapsablePanel - id={packagePolicy.id} - data-test-subj={dataTestSubj} - title={ - <EuiTitle size="xs"> - <h3> - <EuiFlexGroup gutterSize="s" alignItems="center"> - <EuiFlexItem grow={false}> - {packagePolicy.package ? ( - <PackageIcon - packageName={packagePolicy.package.name} - version={packagePolicy.package.version} - size="l" - tryApi={true} - /> - ) : ( - <PackageIcon size="l" packageName="default" version="0" /> - )} - </EuiFlexItem> - <EuiFlexItem className="eui-textTruncate"> - <EuiLink - className="eui-textTruncate" - data-test-subj="agentPolicyDetailsLink" - href={getHref('edit_integration', { - policyId: agentPolicy.id, - packagePolicyId: packagePolicy.id, - })} - > - {packagePolicy.name} - </EuiLink> - </EuiFlexItem> - {showNeedsAttentionBadge && ( + const genericErrorsListExtensionViewWrapper = useMemo(() => { + return ( + genericErrorsListExtensionView && ( + <ExtensionWrapper> + <genericErrorsListExtensionView.Component packageErrors={packageErrors} /> + </ExtensionWrapper> + ) + ); + }, [packageErrors, genericErrorsListExtensionView]); + + return ( + <CollapsablePanel + id={packagePolicy.id} + data-test-subj={dataTestSubj} + title={ + <EuiTitle size="xs"> + <h3> + <EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexItem grow={false}> - <EuiBadge - color={theme.euiTheme.colors.danger} - iconType="warning" - iconSide="left" - data-test-subj={dataTestSubj ? `${dataTestSubj}-needsAttention` : undefined} - > - <FormattedMessage - id="xpack.fleet.agentDetailsIntegrations.needsAttention.label" - defaultMessage="Needs attention" + {packagePolicy.package ? ( + <PackageIcon + packageName={packagePolicy.package.name} + version={packagePolicy.package.version} + size="l" + tryApi={true} /> - </EuiBadge> + ) : ( + <PackageIcon size="l" packageName="default" version="0" /> + )} </EuiFlexItem> - )} - </EuiFlexGroup> - </h3> - </EuiTitle> - } - > - <AgentDetailsIntegrationInputs agent={agent} packagePolicy={packagePolicy} /> - {policyResponseExtensionViewWrapper} - {genericErrorsListExtensionViewWrapper} - <EuiSpacer /> - </CollapsablePanel> - ); -}); + <EuiFlexItem className="eui-textTruncate"> + <EuiLink + className="eui-textTruncate" + data-test-subj="agentPolicyDetailsLink" + href={getHref('edit_integration', { + policyId: agentPolicy.id, + packagePolicyId: packagePolicy.id, + })} + > + {packagePolicy.name} + </EuiLink> + </EuiFlexItem> + {showNeedsAttentionBadge && ( + <EuiFlexItem grow={false}> + <EuiBadge + color={theme.euiTheme.colors.danger} + iconType="warning" + iconSide="left" + data-test-subj={dataTestSubj ? `${dataTestSubj}-needsAttention` : undefined} + > + <FormattedMessage + id="xpack.fleet.agentDetailsIntegrations.needsAttention.label" + defaultMessage="Needs attention" + /> + </EuiBadge> + </EuiFlexItem> + )} + </EuiFlexGroup> + </h3> + </EuiTitle> + } + > + <AgentDetailsIntegrationInputs + agent={agent} + packagePolicy={packagePolicy} + linkToLogs={linkToLogs} + /> + {policyResponseExtensionViewWrapper} + {genericErrorsListExtensionViewWrapper} + <EuiSpacer size="s" /> + </CollapsablePanel> + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx index 71b1a1ad6fe3e..7630b61c08be6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx @@ -66,8 +66,9 @@ const StyledEuiTreeView = styled(EuiTreeView)` export const AgentDetailsIntegrationInputs: React.FunctionComponent<{ agent: Agent; packagePolicy: PackagePolicy; + linkToLogs?: boolean; 'data-test-subj'?: string; -}> = memo(({ agent, packagePolicy, 'data-test-subj': dataTestSubj }) => { +}> = memo(({ agent, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => { const { getHref } = useLink(); const inputStatusMap = useMemo( @@ -138,21 +139,25 @@ export const AgentDetailsIntegrationInputs: React.FunctionComponent<{ defaultMessage: 'View logs', })} > - <StyledEuiLink - href={getHref('agent_details', { - agentId: agent.id, - tabId: 'logs', - logQuery: getLogsQueryByInputType(current.type), - })} - aria-label={i18n.translate( - 'xpack.fleet.agentDetailsIntegrations.viewLogsButton', - { - defaultMessage: 'View logs', - } - )} - > - {displayInputType(current.type)} - </StyledEuiLink> + {linkToLogs ? ( + <StyledEuiLink + href={getHref('agent_details', { + agentId: agent.id, + tabId: 'logs', + logQuery: getLogsQueryByInputType(current.type), + })} + aria-label={i18n.translate( + 'xpack.fleet.agentDetailsIntegrations.viewLogsButton', + { + defaultMessage: 'View logs', + } + )} + > + {displayInputType(current.type)} + </StyledEuiLink> + ) : ( + <>{displayInputType(current.type)}</> + )} </EuiToolTip> ), id: current.type, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index 5a7b135b46440..0a1df537e7bcd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -15,7 +15,8 @@ import { AgentDetailsIntegration } from './agent_details_integration'; export const AgentDetailsIntegrations: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; -}> = memo(({ agent, agentPolicy }) => { + linkToLogs?: boolean; +}> = memo(({ agent, agentPolicy, linkToLogs = true }) => { if (!agentPolicy || !agentPolicy.package_policies) { return null; } @@ -31,6 +32,7 @@ export const AgentDetailsIntegrations: React.FunctionComponent<{ agent={agent} agentPolicy={agentPolicy} packagePolicy={packagePolicy} + linkToLogs={linkToLogs} data-test-subj={`${testSubj}-accordion`} /> </EuiFlexItem> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/dashboards_buttons.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/dashboards_buttons.tsx index 25c394e2606b0..3e50258071576 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/dashboards_buttons.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/dashboards_buttons.tsx @@ -5,49 +5,69 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useQuery } from '@tanstack/react-query'; -import { DASHBOARD_LOCATORS_IDS } from '../../../../../../../common/constants'; +import { + DASHBOARD_LOCATORS_IDS, + FLEET_ELASTIC_AGENT_PACKAGE, +} from '../../../../../../../common/constants'; -import { useDashboardLocator, useStartServices } from '../../../../hooks'; +import { + useDashboardLocator, + useFleetStatus, + useGetPackageInfoByKeyQuery, + useStartServices, +} from '../../../../hooks'; + +import { getDashboardIdForSpace } from '../../services/dashboard_helpers'; const useDashboardExists = (dashboardId: string) => { - const [dashboardExists, setDashboardExists] = React.useState<boolean>(false); - const [loading, setLoading] = React.useState<boolean>(true); const { dashboard: dashboardPlugin } = useStartServices(); - useEffect(() => { - const fetchDashboard = async () => { + const { data, isLoading } = useQuery({ + queryKey: ['dashboard_exists', dashboardId], + queryFn: async () => { try { const findDashboardsService = await dashboardPlugin.findDashboardsService(); const [dashboard] = await findDashboardsService.findByIds([dashboardId]); - setLoading(false); - setDashboardExists(dashboard?.status === 'success'); + return dashboard?.status === 'success'; } catch (e) { - setLoading(false); - setDashboardExists(false); + return false; } - }; - - fetchDashboard(); - }, [dashboardId, dashboardPlugin]); - - return { dashboardExists, loading }; + }, + }); + return { dashboardExists: data ?? false, loading: isLoading }; }; export const DashboardsButtons: React.FunctionComponent = () => { + const { data } = useGetPackageInfoByKeyQuery(FLEET_ELASTIC_AGENT_PACKAGE); + const { spaceId } = useFleetStatus(); + const dashboardLocator = useDashboardLocator(); const getDashboardHref = (dashboardId: string) => { return dashboardLocator?.getRedirectUrl({ dashboardId }) || ''; }; - const { dashboardExists, loading: dashboardLoading } = useDashboardExists( + const elasticAgentOverviewDashboardId = getDashboardIdForSpace( + spaceId, + data, DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_OVERVIEW ); + const elasticAgentInfoDashboardId = getDashboardIdForSpace( + spaceId, + data, + DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_INFO + ); + + const { dashboardExists, loading: dashboardLoading } = useDashboardExists( + elasticAgentOverviewDashboardId + ); + if (dashboardLoading || !dashboardExists) { return null; } @@ -58,7 +78,7 @@ export const DashboardsButtons: React.FunctionComponent = () => { <EuiFlexItem grow={false}> <EuiButtonEmpty iconType="dashboardApp" - href={getDashboardHref(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_OVERVIEW)} + href={getDashboardHref(elasticAgentOverviewDashboardId)} data-test-subj="ingestOverviewLinkButton" > <FormattedMessage @@ -70,7 +90,7 @@ export const DashboardsButtons: React.FunctionComponent = () => { <EuiFlexItem grow={false}> <EuiButtonEmpty iconType="dashboardApp" - href={getDashboardHref(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_INFO)} + href={getDashboardHref(elasticAgentInfoDashboardId)} data-test-subj="agentInfoLinkButton" > <FormattedMessage diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index fa8e263c16170..c7b71af5f97b6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; +import type { EuiBadgeProps } from '@elastic/eui'; import { EuiBadge, EuiButton, @@ -34,67 +35,75 @@ import { useAgentRefresh } from '../agent_details_page/hooks'; import { AgentUpgradeAgentModal } from './agent_upgrade_modal'; -interface Props { +type Props = EuiBadgeProps & { agent: Agent; fromDetails?: boolean; -} - -const Status = { - Healthy: ( - <EuiBadge color="success"> - <FormattedMessage id="xpack.fleet.agentHealth.healthyStatusText" defaultMessage="Healthy" /> - </EuiBadge> - ), - Offline: ( - <EuiBadge color="default"> - <FormattedMessage id="xpack.fleet.agentHealth.offlineStatusText" defaultMessage="Offline" /> - </EuiBadge> - ), - Inactive: ( - <EuiBadge color={euiVars.euiColorDarkShade}> - <FormattedMessage id="xpack.fleet.agentHealth.inactiveStatusText" defaultMessage="Inactive" /> - </EuiBadge> - ), - Unenrolled: ( - <EuiBadge color={euiVars.euiColorDisabled}> - <FormattedMessage - id="xpack.fleet.agentHealth.unenrolledStatusText" - defaultMessage="Unenrolled" - /> - </EuiBadge> - ), - Unhealthy: ( - <EuiBadge color="warning"> - <FormattedMessage - id="xpack.fleet.agentHealth.unhealthyStatusText" - defaultMessage="Unhealthy" - /> - </EuiBadge> - ), - Updating: ( - <EuiBadge color="primary"> - <FormattedMessage id="xpack.fleet.agentHealth.updatingStatusText" defaultMessage="Updating" /> - </EuiBadge> - ), }; -function getStatusComponent(status: Agent['status']): React.ReactElement { +function getStatusComponent({ + status, + ...restOfProps +}: { + status: Agent['status']; +} & EuiBadgeProps): React.ReactElement { switch (status) { case 'error': case 'degraded': - return Status.Unhealthy; + return ( + <EuiBadge color="warning" {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.unhealthyStatusText" + defaultMessage="Unhealthy" + /> + </EuiBadge> + ); case 'inactive': - return Status.Inactive; + return ( + <EuiBadge color={euiVars.euiColorDarkShade} {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.inactiveStatusText" + defaultMessage="Inactive" + /> + </EuiBadge> + ); case 'offline': - return Status.Offline; + return ( + <EuiBadge color="default" {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.offlineStatusText" + defaultMessage="Offline" + /> + </EuiBadge> + ); case 'unenrolling': case 'enrolling': case 'updating': - return Status.Updating; + return ( + <EuiBadge color="primary" {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.updatingStatusText" + defaultMessage="Updating" + /> + </EuiBadge> + ); case 'unenrolled': - return Status.Unenrolled; + return ( + <EuiBadge color={euiVars.euiColorDisabled} {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.unenrolledStatusText" + defaultMessage="Unenrolled" + /> + </EuiBadge> + ); default: - return Status.Healthy; + return ( + <EuiBadge color="success" {...restOfProps}> + <FormattedMessage + id="xpack.fleet.agentHealth.healthyStatusText" + defaultMessage="Healthy" + /> + </EuiBadge> + ); } } @@ -102,7 +111,11 @@ const WrappedEuiCallOut = styled(EuiCallOut)` white-space: wrap !important; `; -export const AgentHealth: React.FunctionComponent<Props> = ({ agent, fromDetails }) => { +export const AgentHealth: React.FunctionComponent<Props> = ({ + agent, + fromDetails, + ...restOfProps +}) => { const { last_checkin: lastCheckIn, last_checkin_message: lastCheckInMessage } = agent; const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); const lastCheckInMessageText = lastCheckInMessage ? ( @@ -172,14 +185,16 @@ export const AgentHealth: React.FunctionComponent<Props> = ({ agent, fromDetails > {isStuckInUpdating(agent) && !fromDetails ? ( <div className="eui-textNoWrap"> - {getStatusComponent(agent.status)} + {getStatusComponent({ status: agent.status, ...restOfProps })}   <EuiIcon type="warning" color="warning" /> </div> ) : ( <> - {getStatusComponent(agent.status)} - {previousToOfflineStatus ? getStatusComponent(previousToOfflineStatus) : null} + {getStatusComponent({ status: agent.status, ...restOfProps })} + {previousToOfflineStatus + ? getStatusComponent({ status: previousToOfflineStatus, ...restOfProps }) + : null} </> )} </EuiToolTip> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helper.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helper.test.ts new file mode 100644 index 0000000000000..cdf654cbeb70e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helper.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetInfoResponse } from '../../../../../../common'; + +import { getDashboardIdForSpace } from './dashboard_helpers'; + +const PKG_INFO = { + item: { + status: 'installed', + installationInfo: { + install_status: 'installed', + installed_kibana_space_id: 'default', + additional_spaces_installed_kibana: { + test: [ + { + id: 'test-destination-1', + originId: 'test-id-1', + }, + ], + }, + }, + }, +} as unknown as GetInfoResponse; + +describe('getDashboardIdForSpace', () => { + it('return the same id if package is installed in the same space', () => { + expect(() => getDashboardIdForSpace('default', PKG_INFO, 'test-id-1')); + }); + + it('return the destination ID if package is installed in an additionnal space', () => { + expect(() => getDashboardIdForSpace('test', PKG_INFO, 'test-id-1')); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helpers.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helpers.ts new file mode 100644 index 0000000000000..bc46118b93fe3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/dashboard_helpers.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; + +import type { GetInfoResponse } from '../../../../../../common'; + +export function getDashboardIdForSpace( + spaceId: string = DEFAULT_SPACE_ID, + res: GetInfoResponse | undefined, + dashboardId: string +) { + if (res?.item?.status !== 'installed') { + return dashboardId; + } + + const installationInfo = res.item.installationInfo; + + if (!installationInfo || installationInfo?.installed_kibana_space_id === spaceId) { + return dashboardId; + } + + return ( + installationInfo.additional_spaces_installed_kibana?.[spaceId]?.find( + ({ originId }) => originId === dashboardId + )?.id ?? dashboardId + ); +} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 9a707500bb03d..0e7e6117d2cf2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -846,7 +846,7 @@ export function Detail() { </Route> <Route path={INTEGRATIONS_ROUTING_PATHS.integration_details_policies}> {canReadIntegrationPolicies ? ( - <PackagePoliciesPage name={packageInfo.name} version={packageInfo.version} /> + <PackagePoliciesPage packageInfo={packageInfo} /> ) : ( <PermissionsError error="MISSING_PRIVILEGES" diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.test.tsx new file mode 100644 index 0000000000000..77ce5720b294d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { screen, fireEvent, act } from '@testing-library/react'; + +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { AgentBasedPackagePoliciesTable } from './agent_based_table'; + +const mockPackagePolicies = [ + { + agentPolicies: [ + { + id: '1', + name: 'Agent Policy 1', + status: 'active' as const, + is_managed: false, + is_protected: false, + updated_at: '2023-01-01T00:00:00Z', + updated_by: 'user', + created_at: '2023-01-01T00:00:00Z', + created_by: 'user', + namespace: 'default', + revision: 1, + monitoring_enabled: [], + }, + ], + packagePolicy: { + id: 'pkg1', + name: 'Package Policy 1', + package: { name: 'package-name', title: 'Package Title', version: '1.0.0' }, + hasUpgrade: true, + inputs: [], + revision: 1, + updated_at: '2023-01-01T00:00:00Z', + updated_by: 'user', + created_at: '2023-01-01T00:00:00Z', + created_by: 'user', + namespace: 'default', + policy_id: 'policy1', + policy_ids: ['policy1'], + enabled: true, + }, + rowIndex: 0, + }, +]; + +const mockPagination = { + pagination: { currentPage: 1, pageSize: 10, totalItemCount: 1, pageSizeOptions: [10, 20, 50] }, + setPagination: jest.fn(), + pageSizeOptions: [10, 20, 50], +}; + +describe('AgentBasedPackagePoliciesTable', () => { + it('renders the table with package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentBasedPackagePoliciesTable + isLoading={false} + packagePolicies={mockPackagePolicies} + packagePoliciesTotal={1} + refreshPackagePolicies={jest.fn()} + pagination={mockPagination} + /> + ); + await act(async () => { + expect(result.getByText('Integration policy')).toBeInTheDocument(); + expect(result.getByText('Package Policy 1')).toBeInTheDocument(); + expect(result.getByText('v1.0.0')).toBeInTheDocument(); + }); + }); + + it('shows loading message when isLoading is true', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentBasedPackagePoliciesTable + isLoading={true} + packagePolicies={[]} + packagePoliciesTotal={0} + refreshPackagePolicies={jest.fn()} + pagination={mockPagination} + /> + ); + await act(async () => { + expect(result.getByText('Loading integration policies…')).toBeInTheDocument(); + }); + }); + + it('shows no policies message when there are no package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentBasedPackagePoliciesTable + isLoading={false} + packagePolicies={[]} + packagePoliciesTotal={0} + refreshPackagePolicies={jest.fn()} + pagination={mockPagination} + /> + ); + await act(async () => { + expect(result.getByText('No integration policies')).toBeInTheDocument(); + }); + }); + + it('opens the agent enrollment flyout when add agent button is clicked', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentBasedPackagePoliciesTable + isLoading={false} + packagePolicies={mockPackagePolicies} + packagePoliciesTotal={1} + refreshPackagePolicies={jest.fn()} + pagination={mockPagination} + /> + ); + + await act(async () => { + fireEvent.click(screen.getByText('Add agent')); + }); + expect(result.getByTestId('agentEnrollmentFlyout')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx new file mode 100644 index 0000000000000..197fa84bf4dd9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx @@ -0,0 +1,346 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { stringify, parse } from 'query-string'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; +import { + EuiBasicTable, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../../types'; +import type { usePagination } from '../../../../../../hooks'; +import { useLink, useAuthz, useMultipleAgentPolicies } from '../../../../../../hooks'; +import { + AgentEnrollmentFlyout, + MultipleAgentPoliciesSummaryLine, + AgentPolicySummaryLine, + PackagePolicyActionsMenu, +} from '../../../../../../components'; + +import { Persona } from '../persona'; + +import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; + +export const AgentBasedPackagePoliciesTable = ({ + isLoading, + packagePolicies, + packagePoliciesTotal, + refreshPackagePolicies, + pagination, + addAgentToPolicyIdFromParams, + showAddAgentHelpForPolicyId, +}: { + isLoading: boolean; + packagePolicies: Array<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }>; + packagePoliciesTotal: number; + refreshPackagePolicies: () => void; + pagination: ReturnType<typeof usePagination>; + addAgentToPolicyIdFromParams?: string | null; + showAddAgentHelpForPolicyId?: string | null; +}) => { + const { getHref } = useLink(); + const { search } = useLocation(); + const history = useHistory(); + + const [selectedTableIndex, setSelectedTableIndex] = useState<number | undefined>(); + const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); + const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; + const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; + const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies; + const canShowMultiplePoliciesCell = + canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies; + + // Show tour help for adding agents to a policy + const addAgentHelpForPolicyId = packagePolicies.find(({ agentPolicies }) => + agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId) + )?.packagePolicy?.id; + + // Handle the "add agent" link displayed in post-installation toast notifications in the case + // where a user is clicking the link while on the package policies listing page + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string | null>( + addAgentToPolicyIdFromParams || null + ); + useEffect(() => { + const unlisten = history.listen((location) => { + const params = new URLSearchParams(location.search); + const addAgentToPolicyId = params.get('addAgentToPolicyId'); + + if (addAgentToPolicyId) { + setFlyoutOpenForPolicyId(addAgentToPolicyId); + } + }); + + return () => unlisten(); + }, [history]); + + const selectedPolicies = + selectedTableIndex !== undefined ? packagePolicies[selectedTableIndex] : undefined; + const selectedAgentPolicies = selectedPolicies?.agentPolicies; + const selectedPackagePolicy = selectedPolicies?.packagePolicy; + const flyoutPolicy = selectedAgentPolicies?.length === 1 ? selectedAgentPolicies[0] : undefined; + + return ( + <> + <EuiBasicTable<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + items={packagePolicies || []} + columns={[ + { + field: 'packagePolicy.name', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { + defaultMessage: 'Integration policy', + }), + render(_, { agentPolicies, packagePolicy }) { + return ( + <EuiLink + className="eui-textTruncate" + data-test-subj="integrationNameLink" + href={getHref('integration_policy_edit', { + packagePolicyId: packagePolicy.id, + })} + > + {packagePolicy.name} + </EuiLink> + ); + }, + }, + { + field: 'packagePolicy.package.version', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { + defaultMessage: 'Version', + }), + render(_version, { agentPolicies, packagePolicy }) { + return ( + <EuiFlexGroup gutterSize="s" alignItems="center" wrap={true}> + <EuiFlexItem grow={false}> + <EuiText + size="s" + className="eui-textNoWrap" + data-test-subj="packageVersionText" + > + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.packageVersion" + defaultMessage="v{version}" + values={{ version: _version }} + /> + </EuiText> + </EuiFlexItem> + + {agentPolicies.length > 0 && packagePolicy.hasUpgrade && ( + <EuiFlexItem grow={false}> + <EuiButton + size="s" + minWidth="0" + href={`${getHref('upgrade_package_policy', { + policyId: agentPolicies[0].id, + packagePolicyId: packagePolicy.id, + })}?from=integrations-policy-list`} + data-test-subj="integrationPolicyUpgradeBtn" + isDisabled={!canWriteIntegrationPolicies} + > + <FormattedMessage + id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeButton" + defaultMessage="Upgrade" + /> + </EuiButton> + </EuiFlexItem> + )} + </EuiFlexGroup> + ); + }, + }, + { + field: 'packagePolicy.policy_ids', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', { + defaultMessage: 'Agent policies', + }), + truncateText: true, + render(ids, { agentPolicies, packagePolicy }) { + return agentPolicies.length > 0 ? ( + canShowMultiplePoliciesCell ? ( + <MultipleAgentPoliciesSummaryLine + policies={agentPolicies} + packagePolicyId={packagePolicy.id} + onAgentPoliciesChange={refreshPackagePolicies} + /> + ) : ( + <AgentPolicySummaryLine policy={agentPolicies[0]} /> + ) + ) : ids.length === 0 ? ( + <EuiText color="subdued" size="xs"> + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.noAgentPolicies" + defaultMessage="No agent policies" + /> + </EuiText> + ) : ( + <EuiText color="subdued" size="xs"> + <EuiIcon size="m" type="warning" color="warning" /> +   + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.agentPolicyDeletedWarning" + defaultMessage="Policy not found" + /> + </EuiText> + ); + }, + }, + { + field: 'packagePolicy.updated_by', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { + defaultMessage: 'Last updated by', + }), + truncateText: true, + render(updatedBy: PackagePolicy['updated_by']) { + return <Persona size="s" name={updatedBy} title={updatedBy} />; + }, + }, + { + field: 'packagePolicy.updated_at', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { + defaultMessage: 'Last updated', + }), + truncateText: true, + render(updatedAt: PackagePolicy['updated_at']) { + return ( + <span className="eui-textTruncate" title={updatedAt}> + <FormattedRelative value={updatedAt} /> + </span> + ); + }, + }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { + defaultMessage: 'Agents', + }), + render({ + agentPolicies, + packagePolicy, + rowIndex, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }) { + if (agentPolicies.length === 0) { + return ( + <EuiText color="subdued" size="xs"> + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.noAgents" + defaultMessage="No agents" + /> + </EuiText> + ); + } + return ( + <PackagePolicyAgentsCell + agentPolicies={agentPolicies} + onAddAgent={() => { + setSelectedTableIndex(rowIndex); + setFlyoutOpenForPolicyId(agentPolicies[0].id); + }} + hasHelpPopover={addAgentHelpForPolicyId === packagePolicy.id} + /> + ); + }, + }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { + defaultMessage: 'Actions', + }), + width: '8ch', + align: 'right', + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + }) { + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies + return ( + <PackagePolicyActionsMenu + agentPolicies={agentPolicies} + packagePolicy={packagePolicy} + showAddAgent={true} + upgradePackagePolicyHref={ + agentPolicy + ? `${getHref('upgrade_package_policy', { + policyId: agentPolicy.id, + packagePolicyId: packagePolicy.id, + })}?from=integrations-policy-list` + : undefined + } + /> + ); + }, + }, + ]} + loading={isLoading} + data-test-subj="integrationPolicyTable" + pagination={{ + pageIndex: pagination.pagination.currentPage - 1, + pageSize: pagination.pagination.pageSize, + totalItemCount: packagePoliciesTotal, + pageSizeOptions: pagination.pageSizeOptions, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + pagination.setPagination({ + currentPage: page.index + 1, + pageSize: page.size, + }); + }} + noItemsMessage={ + isLoading ? ( + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage" + defaultMessage="Loading integration policies…" + /> + ) : ( + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.noPoliciesMessage" + defaultMessage="No integration policies" + /> + ) + } + /> + {flyoutOpenForPolicyId && selectedAgentPolicies && !isLoading && ( + <AgentEnrollmentFlyout + onClose={() => { + setFlyoutOpenForPolicyId(null); + const { addAgentToPolicyId, ...rest } = parse(search); + history.replace({ search: stringify(rest) }); + }} + agentPolicy={flyoutPolicy} + selectedAgentPolicies={selectedAgentPolicies} + isIntegrationFlow={true} + installedPackagePolicy={{ + name: selectedPackagePolicy?.package?.name || '', + version: selectedPackagePolicy?.package?.version || '', + }} + /> + )} + </> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx new file mode 100644 index 0000000000000..25a9933fd7197 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, act, waitFor } from '@testing-library/react'; + +import { AGENTS_PREFIX } from '../../../../../../../../../common/constants'; +import { sendGetAgents } from '../../../../../../hooks'; +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { AgentlessPackagePoliciesTable } from './agentless_table'; + +jest.mock('../../../../../../hooks', () => ({ + ...jest.requireActual('../../../../../../hooks'), + useConfirmForceInstall: jest.fn(), + sendGetAgents: jest.fn(), +})); + +describe('AgentlessPackagePoliciesTable', () => { + const mockSendGetAgents = sendGetAgents as jest.MockedFunction<typeof sendGetAgents>; + + beforeEach(() => { + mockSendGetAgents.mockResolvedValue({ + data: { + items: [ + { + policy_id: 'policy1', + id: 'agent1', + packages: ['package'], + type: 'PERMANENT', + active: true, + enrolled_at: '2023-01-01T00:00:00Z', + local_metadata: {}, + status: 'online', + }, + ], + total: 1, + page: 1, + perPage: 10000, + }, + error: null, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const defaultProps = { + isLoading: false, + packagePolicies: [ + { + agentPolicies: [ + { + id: 'policy1', + name: 'Policy 1', + status: 'active' as const, + is_managed: false, + updated_at: '2023-01-01T00:00:00Z', + updated_by: 'user1', + namespace: 'default', + monitoring_enabled: [], + revision: 1, + is_protected: false, + }, + ], + packagePolicy: { + id: 'packagePolicy1', + name: 'Package Policy 1', + updated_by: 'user1', + updated_at: '2023-01-01T00:00:00Z', + inputs: [], + policy_id: 'policy1', + namespace: 'default', + enabled: true, + package: { + name: 'package', + title: 'Package', + version: '1.0.0', + }, + hasUpgrade: false, + revision: 1, + created_at: '2023-01-01T00:00:00Z', + created_by: 'user1', + policy_ids: ['policy1'], + }, + rowIndex: 0, + }, + ], + packagePoliciesTotal: 1, + refreshPackagePolicies: jest.fn(), + pagination: { + pagination: { currentPage: 1, pageSize: 10 }, + setPagination: jest.fn(), + pageSizeOptions: [10, 20, 50], + }, + }; + + it('shows loading message when isLoading is true', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentlessPackagePoliciesTable {...defaultProps} packagePolicies={[]} isLoading={true} /> + ); + await act(async () => { + expect(result.getByText('Loading integration policies…')).toBeInTheDocument(); + }); + }); + + it('shows no items message when there are no package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + <AgentlessPackagePoliciesTable {...defaultProps} packagePolicies={[]} /> + ); + await act(async () => { + expect(result.getByText('No agentless integration policies')).toBeInTheDocument(); + }); + }); + + it('renders the table with package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />); + + await act(async () => { + expect(result.getByText('Package Policy 1')).toBeInTheDocument(); + expect(result.getByText('user1')).toBeInTheDocument(); + }); + }); + + it('displays agent health status when agents are loaded', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />); + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + perPage: 10000, + kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`, + }); + }); + expect(await result.findByText('Healthy')).toBeInTheDocument(); + }); + + it('opens flyout when status badge is clicked', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />); + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + perPage: 10000, + kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`, + }); + }); + await act(async () => { + fireEvent.click(await result.findByText('Healthy')); + }); + expect(result.getByText('Confirm agentless enrollment')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx new file mode 100644 index 0000000000000..22348f137c513 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx @@ -0,0 +1,300 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useMemo, useState } from 'react'; +import type { HorizontalAlignment } from '@elastic/eui'; +import { EuiBadge, EuiBasicTable, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import type { + Agent, + AgentPolicy, + InMemoryPackagePolicy, + PackagePolicy, +} from '../../../../../../types'; +import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../../../../../../../common/constants'; +import type { usePagination } from '../../../../../../hooks'; +import { useLink, sendGetAgents, useAuthz, useStartServices } from '../../../../../../hooks'; +import { + Loading, + PackagePolicyActionsMenu, + AgentlessEnrollmentFlyout, +} from '../../../../../../components'; + +import { Persona } from '../persona'; +import { AgentHealth } from '../../../../../../../fleet/sections/agents/components'; + +const REFRESH_INTERVAL_MS = 30000; + +export const AgentlessPackagePoliciesTable = ({ + isLoading, + packagePolicies, + packagePoliciesTotal, + refreshPackagePolicies, + pagination, +}: { + isLoading: boolean; + packagePolicies: Array<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }>; + packagePoliciesTotal: number; + refreshPackagePolicies: () => void; + pagination: ReturnType<typeof usePagination>; +}) => { + const core = useStartServices(); + const { notifications } = core; + const authz = useAuthz(); + const { getHref } = useLink(); + const [isAgentsLoading, setIsAgentsLoading] = useState<boolean>(false); + const [agentsByPolicyId, setAgentsByPolicyId] = useState<Record<string, Agent>>({}); + const canReadAgents = authz.fleet.readAgents; + + // Kuery for all agents enrolled into the agent policies associated with the package policies + // We use the first agent policy as agentless package policies have a 1:1 relationship with agent policies + // Maximum # of agent policies is 50, based on the max page size in UI + const agentsKuery = useMemo(() => { + return packagePolicies + .reduce((policyIds, { agentPolicies }) => { + return [...policyIds, ...(agentPolicies[0] ? [agentPolicies[0]?.id] : [])]; + }, [] as string[]) + .map((policyId) => `${AGENTS_PREFIX}.policy_id: "${policyId}"`) + .join(' or '); + }, [packagePolicies]); + + // Fetch agents using above kuery, if the user has access to read agents + // Polls every 30 seconds + useEffect(() => { + const fetchAgents = async () => { + const { data: agentsData, error } = await sendGetAgents({ + perPage: SO_SEARCH_LIMIT, + kuery: agentsKuery, + }); + + setAgentsByPolicyId( + (agentsData?.items || []).reduce((acc, agent) => { + if (agent.policy_id) { + acc[agent.policy_id] = agent; + } + return acc; + }, {} as Record<string, Agent>) + ); + + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError', + { + defaultMessage: 'Error fetching agentless status information', + } + ), + }); + } + setIsAgentsLoading(false); + }; + + if (canReadAgents) { + setIsAgentsLoading(true); + fetchAgents(); + const interval = setInterval(() => { + fetchAgents(); + }, REFRESH_INTERVAL_MS); + return () => clearInterval(interval); + } + }, [agentsKuery, canReadAgents, notifications.toasts]); + + // Flyout state + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string>(); + const [flyoutPackagePolicy, setFlyoutPackagePolicy] = useState<PackagePolicy>(); + const [flyoutAgentPolicy, setFlyoutAgentPolicy] = useState<AgentPolicy>(); + + return ( + <> + <EuiBasicTable<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + items={packagePolicies || []} + columns={[ + { + field: 'packagePolicy.name', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { + defaultMessage: 'Integration policy', + }), + render(_, { agentPolicies, packagePolicy }) { + return ( + <EuiLink + className="eui-textTruncate" + data-test-subj="agentlessIntegrationNameLink" + href={getHref('integration_policy_edit', { + packagePolicyId: packagePolicy.id, + })} + > + {packagePolicy.name} + </EuiLink> + ); + }, + }, + { + field: 'packagePolicy.updated_by', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { + defaultMessage: 'Last updated by', + }), + truncateText: true, + render(updatedBy: PackagePolicy['updated_by']) { + return <Persona size="s" name={updatedBy} title={updatedBy} />; + }, + }, + { + field: 'packagePolicy.updated_at', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { + defaultMessage: 'Last updated', + }), + truncateText: true, + render(updatedAt: PackagePolicy['updated_at']) { + return ( + <span className="eui-textTruncate" title={updatedAt}> + <FormattedRelative value={updatedAt} /> + </span> + ); + }, + }, + ...(canReadAgents + ? [ + { + field: '', + name: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatus', + { + defaultMessage: 'Status', + } + ), + align: 'left' as HorizontalAlignment, + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }) { + if (isAgentsLoading) { + return <Loading size="s" />; + } + // Use the first agent policy ID associated with the package policy + // because agentless package policies are only associated with one agent policy + const agentPolicy = agentPolicies[0]; + const agent = + (agentPolicy?.id && agentsByPolicyId[agentPolicy.id]) || undefined; + + // Status badge click handler + const statusBadgeProps = { + onClick: () => { + setFlyoutOpenForPolicyId(packagePolicy.id); + setFlyoutPackagePolicy(packagePolicy); + setFlyoutAgentPolicy(agentPolicy); + }, + 'data-test-subj': 'agentlessStatusBadge', + onClickAriaLabel: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusAriaLabel', + { + defaultMessage: 'Open status details', + } + ), + }; + + return agent ? ( + <AgentHealth agent={agent} {...statusBadgeProps} /> + ) : ( + <EuiBadge color="default" {...statusBadgeProps}> + <FormattedMessage + id="xpack.fleet.packageDetails.integrationList.pendingAgentlessStatus" + defaultMessage="Pending" + /> + </EuiBadge> + ); + }, + }, + ] + : []), + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { + defaultMessage: 'Actions', + }), + width: '8ch', + align: 'right' as HorizontalAlignment, + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + }) { + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies + return ( + <PackagePolicyActionsMenu + agentPolicies={agentPolicies} + packagePolicy={packagePolicy} + showAddAgent={true} + upgradePackagePolicyHref={ + agentPolicy + ? `${getHref('upgrade_package_policy', { + policyId: agentPolicy.id, + packagePolicyId: packagePolicy.id, + })}?from=integrations-policy-list` + : undefined + } + /> + ); + }, + }, + ]} + loading={isLoading} + data-test-subj="integrationPolicyTable" + pagination={{ + pageIndex: pagination.pagination.currentPage - 1, + pageSize: pagination.pagination.pageSize, + totalItemCount: packagePoliciesTotal, + pageSizeOptions: pagination.pageSizeOptions, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + pagination.setPagination({ + currentPage: page.index + 1, + pageSize: page.size, + }); + }} + noItemsMessage={ + isLoading ? ( + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage" + defaultMessage="Loading integration policies…" + /> + ) : ( + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.noAgentlessPoliciesMessage" + defaultMessage="No agentless integration policies" + /> + ) + } + /> + {flyoutOpenForPolicyId && flyoutPackagePolicy && ( + <AgentlessEnrollmentFlyout + onClose={() => { + setFlyoutOpenForPolicyId(undefined); + setFlyoutPackagePolicy(undefined); + setFlyoutAgentPolicy(undefined); + }} + packagePolicy={flyoutPackagePolicy} + agentPolicy={flyoutAgentPolicy} + /> + )} + </> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 300de597f6900..06096b1c3de5b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -4,80 +4,49 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { stringify, parse } from 'query-string'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { Redirect, useLocation, useHistory } from 'react-router-dom'; -import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; import { - EuiBasicTable, - EuiLink, + EuiAccordion, EuiFlexGroup, EuiFlexItem, + EuiNotificationBadge, + EuiPanel, + EuiSpacer, EuiText, - EuiButton, - EuiIcon, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import { FormattedMessage } from '@kbn/i18n-react'; import { InstallStatus } from '../../../../../types'; -import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; +import type { + AgentPolicy, + GetAgentPoliciesResponseItem, + InMemoryPackagePolicy, + PackageInfo, + PackagePolicy, +} from '../../../../../types'; import { useLink, - useUrlPagination, useGetPackageInstallStatus, AgentPolicyRefreshContext, useIsPackagePolicyUpgradable, - useAuthz, - useMultipleAgentPolicies, + usePagination, } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { - AgentEnrollmentFlyout, - MultipleAgentPoliciesSummaryLine, - AgentPolicySummaryLine, - PackagePolicyActionsMenu, -} from '../../../../../components'; import { SideBarColumn } from '../../../components/side_bar_column'; -import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell'; -import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; -import { Persona } from './persona'; - -interface PackagePoliciesPanelProps { - name: string; - version: string; -} - -interface InMemoryPackagePolicyAndAgentPolicy { - packagePolicy: InMemoryPackagePolicy; - agentPolicies: GetAgentPoliciesResponseItem[]; - rowIndex: number; -} +import { useAgentless } from '../../../../../../fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology'; -const IntegrationDetailsLink = memo<{ - packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']; - agentPolicies: InMemoryPackagePolicyAndAgentPolicy['agentPolicies']; -}>(({ packagePolicy }) => { - const { getHref } = useLink(); - return ( - <EuiLink - className="eui-textTruncate" - data-test-subj="integrationNameLink" - href={getHref('integration_policy_edit', { - packagePolicyId: packagePolicy.id, - })} - > - {packagePolicy.name} - </EuiLink> - ); -}); +import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; +import { AgentBasedPackagePoliciesTable } from './components/agent_based_table'; +import { AgentlessPackagePoliciesTable } from './components/agentless_table'; -export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { +export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo }) => { + const { name, version } = packageInfo; const { search } = useLocation(); - const history = useHistory(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const agentPolicyIdFromParams = useMemo( + const addAgentToPolicyIdFromParams = useMemo( () => queryParams.get('addAgentToPolicyId'), [queryParams] ); @@ -85,44 +54,27 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps () => queryParams.get('showAddAgentHelpForPolicyId'), [queryParams] ); - const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string | null>( - agentPolicyIdFromParams - ); - const [selectedTableIndex, setSelectedTableIndex] = useState<number | undefined>(); - - const { getPath, getHref } = useLink(); + const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); - const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); - const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); - const { - data, - isLoading, - resendRequest: refreshPolicies, - } = usePackagePoliciesWithAgentPolicy({ - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, - }); const { isPackagePolicyUpgradable } = useIsPackagePolicyUpgradable(); + const { isAgentlessIntegration } = useAgentless(); + const canHaveAgentlessPolicies = useMemo( + () => isAgentlessIntegration(packageInfo), + [isAgentlessIntegration, packageInfo] + ); - const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; - const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; - const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies; - - const packageAndAgentPolicies = useMemo((): Array<{ - agentPolicies: GetAgentPoliciesResponseItem[]; - packagePolicy: InMemoryPackagePolicy; - rowIndex: number; - }> => { - if (!data?.items) { - return []; - } - - const newPolicies = data.items.map(({ agentPolicies, packagePolicy }, index) => { + // Helper function to map raw policies data for consumption by the table + const mapPoliciesData = useCallback( + ( + { + agentPolicies, + packagePolicy, + }: { agentPolicies: AgentPolicy[]; packagePolicy: PackagePolicy }, + index: number + ) => { const hasUpgrade = isPackagePolicyUpgradable(packagePolicy); - return { agentPolicies, packagePolicy: { @@ -131,284 +83,204 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }, rowIndex: index, }; - }); - - return newPolicies; - }, [data?.items, isPackagePolicyUpgradable]); - - const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find(({ agentPolicies }) => - agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId) - )?.packagePolicy?.id; - // Handle the "add agent" link displayed in post-installation toast notifications in the case - // where a user is clicking the link while on the package policies listing page - useEffect(() => { - const unlisten = history.listen((location) => { - const params = new URLSearchParams(location.search); - const addAgentToPolicyId = params.get('addAgentToPolicyId'); - - if (addAgentToPolicyId) { - setFlyoutOpenForPolicyId(addAgentToPolicyId); - } - }); - - return () => unlisten(); - }, [history]); - - const handleTableOnChange = useCallback( - ({ page }: CriteriaWithPagination<InMemoryPackagePolicyAndAgentPolicy>) => { - setPagination({ - currentPage: page.index + 1, - pageSize: page.size, - }); }, - [setPagination] + [isPackagePolicyUpgradable] ); - const canShowMultiplePoliciesCell = - canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies; - const columns: Array<EuiTableFieldDataColumnType<InMemoryPackagePolicyAndAgentPolicy>> = useMemo( - () => [ - { - field: 'packagePolicy.name', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { - defaultMessage: 'Integration policy', - }), - render(_, { agentPolicies, packagePolicy }) { - return ( - <IntegrationDetailsLink packagePolicy={packagePolicy} agentPolicies={agentPolicies} /> - ); - }, - }, - { - field: 'packagePolicy.package.version', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { - defaultMessage: 'Version', - }), - render(_version, { agentPolicies, packagePolicy }) { - return ( - <EuiFlexGroup gutterSize="s" alignItems="center" wrap={true}> - <EuiFlexItem grow={false}> - <EuiText size="s" className="eui-textNoWrap" data-test-subj="packageVersionText"> - <FormattedMessage - id="xpack.fleet.epm.packageDetails.integrationList.packageVersion" - defaultMessage="v{version}" - values={{ version: _version }} - /> - </EuiText> - </EuiFlexItem> - {agentPolicies.length > 0 && packagePolicy.hasUpgrade && ( - <EuiFlexItem grow={false}> - <EuiButton - size="s" - minWidth="0" - href={`${getHref('upgrade_package_policy', { - policyId: agentPolicies[0].id, - packagePolicyId: packagePolicy.id, - })}?from=integrations-policy-list`} - data-test-subj="integrationPolicyUpgradeBtn" - isDisabled={!canWriteIntegrationPolicies} - > - <FormattedMessage - id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeButton" - defaultMessage="Upgrade" - /> - </EuiButton> - </EuiFlexItem> - )} - </EuiFlexGroup> - ); - }, - }, - { - field: 'packagePolicy.policy_ids', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', { - defaultMessage: 'Agent policies', - }), - truncateText: true, - render(ids, { agentPolicies, packagePolicy }) { - return agentPolicies.length > 0 ? ( - canShowMultiplePoliciesCell ? ( - <MultipleAgentPoliciesSummaryLine - policies={agentPolicies} - packagePolicyId={packagePolicy.id} - onAgentPoliciesChange={refreshPolicies} - /> - ) : ( - <AgentPolicySummaryLine policy={agentPolicies[0]} /> - ) - ) : ids.length === 0 ? ( - <EuiText color="subdued" size="xs"> - <FormattedMessage - id="xpack.fleet.epm.packageDetails.integrationList.noAgentPolicies" - defaultMessage="No agent policies" - /> - </EuiText> - ) : ( - <EuiText color="subdued" size="xs"> - <EuiIcon size="m" type="warning" color="warning" /> -   - <FormattedMessage - id="xpack.fleet.epm.packageDetails.integrationList.agentPolicyDeletedWarning" - defaultMessage="Policy not found" - /> - </EuiText> - ); - }, - }, - { - field: 'packagePolicy.updated_by', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { - defaultMessage: 'Last updated by', - }), - truncateText: true, - render(updatedBy) { - return <Persona size="s" name={updatedBy} title={updatedBy} />; - }, - }, - { - field: 'packagePolicy.updated_at', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { - defaultMessage: 'Last updated', - }), - truncateText: true, - render(updatedAt: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']['updated_at']) { - return ( - <span className="eui-textTruncate" title={updatedAt}> - <FormattedRelative value={updatedAt} /> - </span> - ); - }, - }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { - defaultMessage: 'Agents', - }), - render({ agentPolicies, packagePolicy, rowIndex }: InMemoryPackagePolicyAndAgentPolicy) { - if (agentPolicies.length === 0) { - return ( - <EuiText color="subdued" size="xs"> - <FormattedMessage - id="xpack.fleet.epm.packageDetails.integrationList.noAgents" - defaultMessage="No agents" - /> - </EuiText> - ); - } - return ( - <PackagePolicyAgentsCell - agentPolicies={agentPolicies} - onAddAgent={() => { - setSelectedTableIndex(rowIndex); - setFlyoutOpenForPolicyId(agentPolicies[0].id); - }} - hasHelpPopover={showAddAgentHelpForPackagePolicyId === packagePolicy.id} - /> - ); - }, - }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { - defaultMessage: 'Actions', - }), - width: '8ch', - align: 'right', - render({ agentPolicies, packagePolicy }) { - const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies - return ( - <PackagePolicyActionsMenu - agentPolicies={agentPolicies} - packagePolicy={packagePolicy} - showAddAgent={true} - upgradePackagePolicyHref={ - agentPolicy - ? `${getHref('upgrade_package_policy', { - policyId: agentPolicy.id, - packagePolicyId: packagePolicy.id, - })}?from=integrations-policy-list` - : undefined - } - /> - ); - }, - }, - ], - [ - getHref, - canWriteIntegrationPolicies, - canShowMultiplePoliciesCell, - showAddAgentHelpForPackagePolicyId, - refreshPolicies, - ] - ); - - const noItemsMessage = useMemo(() => { - return isLoading ? ( - <FormattedMessage - id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage" - defaultMessage="Loading integration policies…" - /> - ) : undefined; - }, [isLoading]); + // States and data for agent-based policies table + // If agentless is not supported or not an agentless integration, skip the + // conditional in the kuery + const { + pagination: agentBasedPagination, + pageSizeOptions: agentBasedPageSizeOptions, + setPagination: agentBasedSetPagination, + } = usePagination(); + const [agentBasedPackageAndAgentPolicies, setAgentBasedPackageAndAgentPolicies] = useState< + Array<{ + agentPolicies: GetAgentPoliciesResponseItem[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + >([]); + const { + data: agentBasedData, + isLoading: agentBasedIsLoading, + resendRequest: refreshAgentBasedPolicies, + } = usePackagePoliciesWithAgentPolicy({ + page: agentBasedPagination.currentPage, + perPage: agentBasedPagination.pageSize, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" ${ + canHaveAgentlessPolicies + ? `AND NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true` + : `` + }`, + }); + useEffect(() => { + setAgentBasedPackageAndAgentPolicies( + !agentBasedData?.items ? [] : agentBasedData.items.map(mapPoliciesData) + ); + }, [agentBasedData, mapPoliciesData]); - const tablePagination = useMemo(() => { - return { - pageIndex: pagination.currentPage - 1, - pageSize: pagination.pageSize, - totalItemCount: data?.total ?? 0, - pageSizeOptions, - }; - }, [data?.total, pageSizeOptions, pagination.currentPage, pagination.pageSize]); + // States and data for agentless policies table + // If agentless is not supported or not an agentless integration, this block and + // initial request is unnessary but reduces code complexity + const { + pagination: agentlessPagination, + pageSizeOptions: agentlessPageSizeOptions, + setPagination: agentlessSetPagination, + } = usePagination(); + const [agentlessPackageAndAgentPolicies, setAgentlessPackageAndAgentPolicies] = useState< + Array<{ + agentPolicies: GetAgentPoliciesResponseItem[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + >([]); + const { + data: agentlessData, + isLoading: agentlessIsLoading, + resendRequest: refreshAgentlessPolicies, + } = usePackagePoliciesWithAgentPolicy({ + page: agentlessPagination.currentPage, + perPage: agentlessPagination.pageSize, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true`, + }); + useEffect(() => { + setAgentlessPackageAndAgentPolicies( + !agentlessData?.items ? [] : agentlessData.items.map(mapPoliciesData) + ); + }, [agentlessData, mapPoliciesData]); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - // Check flyoutOpenForPolicyId otherwise right after installing a new integration the flyout won't open - if (packageInstallStatus.status !== InstallStatus.installed && !flyoutOpenForPolicyId) { + // Check `addAgentToPolicyIdFromParams` otherwise right after installing a new integration the flyout won't open + if (packageInstallStatus.status !== InstallStatus.installed && !addAgentToPolicyIdFromParams) { return ( <Redirect to={getPath('integration_details_overview', { pkgkey: `${name}-${version}` })} /> ); } - const selectedPolicies = - selectedTableIndex !== undefined ? packageAndAgentPolicies[selectedTableIndex] : undefined; - - const agentPolicies = selectedPolicies?.agentPolicies; - const packagePolicy = selectedPolicies?.packagePolicy; - const flyoutPolicy = agentPolicies?.length === 1 ? agentPolicies[0] : undefined; - return ( - <AgentPolicyRefreshContext.Provider value={{ refresh: refreshPolicies }}> + <AgentPolicyRefreshContext.Provider + value={{ + refresh: () => { + refreshAgentBasedPolicies(); + refreshAgentlessPolicies(); + }, + }} + > <EuiFlexGroup alignItems="flexStart"> <SideBarColumn grow={1} /> <EuiFlexItem grow={7}> - <EuiBasicTable - items={packageAndAgentPolicies || []} - columns={columns} - loading={isLoading} - data-test-subj="integrationPolicyTable" - pagination={tablePagination} - onChange={handleTableOnChange} - noItemsMessage={noItemsMessage} - /> + {!canHaveAgentlessPolicies ? ( + <AgentBasedPackagePoliciesTable + isLoading={agentBasedIsLoading} + packagePolicies={agentBasedPackageAndAgentPolicies} + packagePoliciesTotal={agentBasedData?.total ?? 0} + refreshPackagePolicies={refreshAgentBasedPolicies} + pagination={{ + pagination: agentBasedPagination, + pageSizeOptions: agentBasedPageSizeOptions, + setPagination: agentBasedSetPagination, + }} + addAgentToPolicyIdFromParams={addAgentToPolicyIdFromParams} + showAddAgentHelpForPolicyId={showAddAgentHelpForPolicyId} + /> + ) : ( + <> + <EuiAccordion + id="agentBasedAccordion" + initialIsOpen={true} + buttonContent={ + <EuiFlexGroup + justifyContent="center" + alignItems="center" + gutterSize="s" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiText size="m"> + <h3> + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.agentlessHeader" + defaultMessage="Agentless" + /> + </h3> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiNotificationBadge color="subdued" size="m"> + <h3>{agentlessData?.total ?? 0}</h3> + </EuiNotificationBadge> + </EuiFlexItem> + </EuiFlexGroup> + } + > + <EuiSpacer size="m" /> + <EuiPanel hasBorder={true} hasShadow={false}> + <AgentlessPackagePoliciesTable + isLoading={agentlessIsLoading} + packagePolicies={agentlessPackageAndAgentPolicies} + packagePoliciesTotal={agentlessData?.total ?? 0} + refreshPackagePolicies={refreshAgentlessPolicies} + pagination={{ + pagination: agentlessPagination, + pageSizeOptions: agentlessPageSizeOptions, + setPagination: agentlessSetPagination, + }} + /> + </EuiPanel> + </EuiAccordion> + <EuiSpacer size="l" /> + <EuiAccordion + id="agentBasedAccordion" + initialIsOpen={true} + buttonContent={ + <EuiFlexGroup + justifyContent="center" + alignItems="center" + gutterSize="s" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiText size="m"> + <h3> + <FormattedMessage + id="xpack.fleet.epm.packageDetails.integrationList.agentBasedHeader" + defaultMessage="Agent-based" + /> + </h3> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiNotificationBadge color="subdued" size="m"> + <h3>{agentBasedData?.total ?? 0}</h3> + </EuiNotificationBadge> + </EuiFlexItem> + </EuiFlexGroup> + } + > + <EuiSpacer size="m" /> + <EuiPanel hasBorder={true} hasShadow={false}> + <AgentBasedPackagePoliciesTable + isLoading={agentBasedIsLoading} + packagePolicies={agentBasedPackageAndAgentPolicies} + packagePoliciesTotal={agentBasedData?.total ?? 0} + refreshPackagePolicies={refreshAgentBasedPolicies} + pagination={{ + pagination: agentBasedPagination, + pageSizeOptions: agentBasedPageSizeOptions, + setPagination: agentBasedSetPagination, + }} + addAgentToPolicyIdFromParams={addAgentToPolicyIdFromParams} + showAddAgentHelpForPolicyId={showAddAgentHelpForPolicyId} + /> + </EuiPanel> + </EuiAccordion> + </> + )} </EuiFlexItem> </EuiFlexGroup> - {flyoutOpenForPolicyId && agentPolicies && !isLoading && ( - <AgentEnrollmentFlyout - onClose={() => { - setFlyoutOpenForPolicyId(null); - const { addAgentToPolicyId, ...rest } = parse(search); - history.replace({ search: stringify(rest) }); - }} - agentPolicy={flyoutPolicy} - selectedAgentPolicies={agentPolicies} - isIntegrationFlow={true} - installedPackagePolicy={{ - name: packagePolicy?.package?.name || '', - version: packagePolicy?.package?.version || '', - }} - /> - )} </AgentPolicyRefreshContext.Provider> ); }; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx index 2a111e5d638a9..412b221e88d47 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx @@ -30,7 +30,7 @@ export const ConfirmIncomingData: React.FunctionComponent<Props> = ({ setAgentDataConfirmed, troubleshootLink, }) => { - const { incomingData, isLoading } = usePollingIncomingData(agentIds); + const { incomingData, isLoading } = usePollingIncomingData({ agentIds }); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(installedPolicy?.name); const { guidedOnboarding } = useStartServices(); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx index 19034151d80a1..be08dbd186e5a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx @@ -75,18 +75,26 @@ export const useGetAgentIncomingData = ( }; const POLLING_INTERVAL_MS = 5 * 1000; // 5 sec -const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min +export const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min /** - * Hook for polling incoming data for the selected agent policy. + * Hook for polling incoming data for the selected agent(s). * @param agentIds * @returns incomingData, isLoading */ -export const usePollingIncomingData = ( - agentIds: string[], - previewData?: boolean, - stopPollingAfterPreviewLength: number = 0 -) => { +export const usePollingIncomingData = ({ + agentIds, + pkgName, + pkgVersion, + previewData, + stopPollingAfterPreviewLength = 0, +}: { + agentIds: string[]; + pkgName?: string; + pkgVersion?: string; + previewData?: boolean; + stopPollingAfterPreviewLength?: number; +}) => { const timeout = useRef<number | undefined>(undefined); const [result, setResult] = useState<{ incomingData: IncomingDataList[]; @@ -117,7 +125,12 @@ export const usePollingIncomingData = ( setHasReachedTimeout(true); } - const { data } = await sendGetAgentIncomingData({ agentsIds: agentIds, previewData }); + const { data } = await sendGetAgentIncomingData({ + agentsIds: agentIds, + previewData, + pkgName, + pkgVersion, + }); if (data?.items) { // filter out agents that have `data = false` and keep polling const filtered = data?.items.filter((item) => { @@ -153,7 +166,15 @@ export const usePollingIncomingData = ( return () => { isAborted = true; }; - }, [agentIds, result, previewData, stopPollingAfterPreviewLength, startedPollingAt]); + }, [ + agentIds, + result, + previewData, + stopPollingAfterPreviewLength, + startedPollingAt, + pkgName, + pkgVersion, + ]); return { ...result, diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx new file mode 100644 index 0000000000000..ef3eb2b0da3cb --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { act, waitFor } from '@testing-library/react'; + +import { sendGetAgents, useGetPackageInfoByKeyQuery } from '../../hooks'; +import { usePollingIncomingData } from '../agent_enrollment_flyout/use_get_agent_incoming_data'; +import { createIntegrationsTestRendererMock } from '../../mock'; + +import { AGENTS_PREFIX } from '../../constants'; + +import type { PackagePolicy } from '../../types'; + +import { AgentlessEnrollmentFlyout } from '.'; + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + useGetPackageInfoByKeyQuery: jest.fn(), + sendGetAgents: jest.fn(), +})); + +jest.mock('../agent_enrollment_flyout/use_get_agent_incoming_data', () => ({ + usePollingIncomingData: jest.fn(), +})); + +const mockSendGetAgents = sendGetAgents as jest.Mock; +const mockUseGetPackageInfoByKeyQuery = useGetPackageInfoByKeyQuery as jest.Mock; +const mockUsePollingIncomingData = usePollingIncomingData as jest.Mock; + +describe('AgentlessEnrollmentFlyout', () => { + const onClose = jest.fn(); + const packagePolicy: PackagePolicy = { + id: 'test-package-policy-id', + name: 'test-package-policy', + namespace: 'default', + policy_ids: ['test-policy-id'], + policy_id: 'test-policy-id', + enabled: true, + output_id: '', + package: { name: 'test-package', title: 'Test Package', version: '1.0.0' }, + inputs: [{ enabled: true, policy_template: 'test-template', type: 'test-type', streams: [] }], + revision: 1, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + }; + + beforeEach(() => { + mockSendGetAgents.mockResolvedValue({ data: { items: [] } }); + mockUseGetPackageInfoByKeyQuery.mockReturnValue({ data: { item: { title: 'Test Package' } } }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the flyout with initial loading state', async () => { + const renderer = createIntegrationsTestRendererMock(); + const { getByText } = renderer.render( + <AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} /> + ); + await act(async () => { + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 is loading')).toBeInTheDocument(); + expect( + getByText('Listening for agentless connection... this could take several minutes') + ).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is disabled')).toBeInTheDocument(); + }); + }); + + it('updates step statuses when agent deployment fails', async () => { + const renderer = createIntegrationsTestRendererMock(); + const agentData = { status: 'error' }; + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } }); + + const { getByText } = renderer.render( + <AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} /> + ); + + await waitFor(() => { + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 has errors')).toBeInTheDocument(); + expect(getByText('Agentless deployment failed')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is disabled')).toBeInTheDocument(); + }); + }); + + it('fetches agents data on mount and sets step statuses when agent deployment succeeds', async () => { + const renderer = createIntegrationsTestRendererMock(); + const agentData = { status: 'online' }; + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: false }); + + const { getByText } = renderer.render( + <AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} /> + ); + + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + kuery: `${AGENTS_PREFIX}.policy_id: "test-policy-id"`, + }); + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Agentless deployment was successful')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is loading')).toBeInTheDocument(); + }); + }); + + it('shows confirm data step as failed when timeout has been reached', async () => { + const renderer = createIntegrationsTestRendererMock(); + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: true }); + + const { getByText } = renderer.render( + <AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} /> + ); + + await waitFor(() => { + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 has errors')).toBeInTheDocument(); + expect(getByText('No incoming data received from agentless integration')).toBeInTheDocument(); + }); + }); + + it('shows confirm data step as successful when incoming data is received', async () => { + const renderer = createIntegrationsTestRendererMock(); + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [{ data: 'test-data' }] }); + + const { getByText } = renderer.render( + <AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} /> + ); + + await waitFor(() => { + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is complete')).toBeInTheDocument(); + expect(getByText('Incoming data received from agentless integration')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx new file mode 100644 index 0000000000000..0fbf8d278fab8 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import type { EuiStepStatus } from '@elastic/eui'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFlyoutFooter, + EuiSteps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { AGENTS_PREFIX, MAX_FLYOUT_WIDTH } from '../../constants'; +import type { Agent, AgentPolicy, PackagePolicy } from '../../types'; +import { sendGetAgents, useStartServices, useGetPackageInfoByKeyQuery } from '../../hooks'; + +import { AgentlessStepConfirmEnrollment } from './step_confirm_enrollment'; +import { AgentlessStepConfirmData } from './step_confirm_data'; + +const REFRESH_INTERVAL_MS = 30000; + +/** + * This component displays additional status details of an agentless agent enrolled + * the chosen package policy (and its agent policy). + * It also displays confirmation that the agentless agent is ingesting data from + * the chosen package policy. + */ +export const AgentlessEnrollmentFlyout = ({ + onClose, + packagePolicy, + agentPolicy, +}: { + onClose: () => void; + packagePolicy: PackagePolicy; + agentPolicy?: AgentPolicy; +}) => { + const core = useStartServices(); + const { notifications } = core; + const [confirmEnrollmentStatus, setConfirmEnrollmentStatus] = useState<EuiStepStatus>('loading'); + const [confirmDataStatus, setConfirmDataStatus] = useState<EuiStepStatus>('disabled'); + const [agentData, setAgentData] = useState<Agent>(); + + // Clear agent data polling + // Called when component is unmounted or when agent is healthy + const agentDataInterval = useRef<NodeJS.Timeout>(); + const clearAgentDataPolling = useMemo(() => { + return () => { + if (agentDataInterval.current) { + clearInterval(agentDataInterval.current); + } + }; + }, [agentDataInterval]); + + // Fetch agent(s) data for the first associated agent policy + // Polls every 30 seconds until agent is found and healthy + useEffect(() => { + const fetchAgents = async () => { + const { data: agentsData, error } = await sendGetAgents({ + kuery: `${AGENTS_PREFIX}.policy_id: "${packagePolicy.policy_ids[0]}"`, + }); + + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError', + { + defaultMessage: 'Error fetching agentless status information', + } + ), + }); + } + + if (agentsData?.items?.[0]) { + setAgentData(agentsData.items?.[0]); + } + }; + + fetchAgents(); + agentDataInterval.current = setInterval(() => { + fetchAgents(); + }, REFRESH_INTERVAL_MS); + + return () => clearAgentDataPolling(); + }, [clearAgentDataPolling, notifications.toasts, packagePolicy.policy_ids]); + + // Watches agent data and updates step statuses and clears polling when agent is healthy + useEffect(() => { + if (agentData) { + if (agentData.status === 'online') { + setConfirmEnrollmentStatus('complete'); + setConfirmDataStatus('loading'); + clearAgentDataPolling(); + } else if (agentData.status === 'error' || agentData.status === 'degraded') { + setConfirmEnrollmentStatus('danger'); + setConfirmDataStatus('disabled'); + } else { + setConfirmEnrollmentStatus('loading'); + setConfirmDataStatus('disabled'); + } + } else { + setConfirmEnrollmentStatus('loading'); + setConfirmDataStatus('disabled'); + } + }, [agentData, clearAgentDataPolling]); + + // Calculate integration title from the base package info and what + // is configured on the package policy. + const { data: packageInfoData } = useGetPackageInfoByKeyQuery( + packagePolicy.package!.name, + packagePolicy.package!.version, + { + prerelease: true, + } + ); + + const integrationTitle = useMemo(() => { + if (packageInfoData?.item) { + const enabledInputs = packagePolicy.inputs?.filter((input) => input.enabled); + + // If only one input is enabled, find the input name from the package info and + // and use that for integration title. Otherwise, use the package name. + if (enabledInputs.length === 1 && enabledInputs[0].policy_template) { + const policyTemplate = packageInfoData.item.policy_templates?.find( + (template) => template.name === enabledInputs[0].policy_template + ); + const input = + policyTemplate && 'inputs' in policyTemplate + ? policyTemplate.inputs?.find((i) => i.type === enabledInputs[0].type) + : null; + return input?.title || packageInfoData.item.title; + } else { + return packageInfoData.item.title; + } + } + return packagePolicy.name; + }, [packageInfoData, packagePolicy]); + + return ( + <EuiFlyout + data-test-subj="agentlessEnrollmentFlyout" + onClose={onClose} + maxWidth={MAX_FLYOUT_WIDTH} + > + <EuiFlyoutHeader hasBorder aria-labelledby="FleetAgentlessEnrollmentFlyoutTitle"> + <EuiTitle size="m"> + <h2 id="FleetAgentlessEnrollmentFlyoutTitle">{packagePolicy.name}</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiSteps + steps={[ + { + title: i18n.translate( + 'xpack.fleet.agentlessEnrollmentFlyout.stepConfirmEnrollmentTitle', + { + defaultMessage: 'Confirm agentless enrollment', + } + ), + children: ( + <AgentlessStepConfirmEnrollment + agent={agentData} + agentPolicy={agentPolicy} + integrationTitle={integrationTitle} + /> + ), + status: confirmEnrollmentStatus, + }, + { + title: i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.stepConfirmDataTitle', { + defaultMessage: 'Confirm incoming data', + }), + children: + agentData && confirmEnrollmentStatus === 'complete' ? ( + <AgentlessStepConfirmData + agent={agentData} + packagePolicy={packagePolicy} + setConfirmDataStatus={setConfirmDataStatus} + /> + ) : ( + <></> // Avoids React error about null children prop + ), + status: confirmDataStatus, + }, + ]} + /> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="flexStart"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={onClose}> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.closeFlyoutButtonLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx new file mode 100644 index 0000000000000..34e69d8ef839a --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { EuiStepStatus } from '@elastic/eui'; +import { EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { useStartServices } from '../../hooks'; +import type { Agent, PackagePolicy } from '../../types'; +import { + usePollingIncomingData, + POLLING_TIMEOUT_MS, +} from '../agent_enrollment_flyout/use_get_agent_incoming_data'; + +export const AgentlessStepConfirmData = ({ + agent, + packagePolicy, + setConfirmDataStatus, +}: { + agent: Agent; + packagePolicy: PackagePolicy; + setConfirmDataStatus: (status: EuiStepStatus) => void; +}) => { + const { docLinks } = useStartServices(); + const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending'); + + // Fetch integration data for the given agent and package policy + const { incomingData, hasReachedTimeout } = usePollingIncomingData({ + agentIds: [agent.id], + pkgName: packagePolicy.package!.name, + pkgVersion: packagePolicy.package!.version, + }); + + // Calculate overall UI state from polling data + useEffect(() => { + if (incomingData.length > 0) { + setConfirmDataStatus('complete'); + setOverallState('success'); + } else if (hasReachedTimeout) { + setConfirmDataStatus('danger'); + setOverallState('failure'); + } else { + setConfirmDataStatus('loading'); + setOverallState('pending'); + } + }, [incomingData, hasReachedTimeout, setConfirmDataStatus]); + + if (overallState === 'success') { + return ( + <EuiCallOut + color="success" + title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.successText', { + defaultMessage: 'Incoming data received from agentless integration', + })} + iconType="check" + /> + ); + } else if (overallState === 'failure') { + return ( + <> + <EuiCallOut + color="danger" + title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.failureText', { + defaultMessage: 'No incoming data received from agentless integration', + })} + iconType="warning" + /> + <EuiSpacer size="m" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmData.failureHelperText" + defaultMessage="No integration data receieved in the past {num} minutes. Check out the {troubleshootingGuideLink} for help." + values={{ + num: POLLING_TIMEOUT_MS / 1000 / 60, + troubleshootingGuideLink: ( + <EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank"> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmData.pendingHelperText.troubleshootingLinkLabel" + defaultMessage="troubleshooting guide" + /> + </EuiLink> + ), + }} + /> + </p> + </EuiText> + </> + ); + } + + return null; +}; diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx new file mode 100644 index 0000000000000..f8a5832e04875 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiPanel, EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import type { Agent, AgentPolicy } from '../../types'; +import { useStartServices } from '../../hooks'; +import { AgentDetailsIntegrations } from '../../applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations'; + +export const AgentlessStepConfirmEnrollment = ({ + agent, + agentPolicy, + integrationTitle, +}: { + agent?: Agent; + agentPolicy?: AgentPolicy; + integrationTitle: string; +}) => { + const { docLinks } = useStartServices(); + const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending'); + + // Calculate overall UI state from agent status + useEffect(() => { + if (agent && agent.status === 'online') { + setOverallState('success'); + } else if (agent && (agent.status === 'error' || agent.status === 'degraded')) { + setOverallState('failure'); + } else { + setOverallState('pending'); + } + }, [agent]); + + if (overallState === 'success') { + return ( + <> + <EuiCallOut + color="success" + title={i18n.translate( + 'xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.successText', + { + defaultMessage: 'Agentless deployment was successful', + } + )} + iconType="check" + /> + <EuiSpacer size="m" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.successHelperText" + defaultMessage="{integrationTitle} agentless integration has been successfully established. You can now seamlessly monitor and manage your {integrationTitle} resources without the need for any additional agents." + values={{ + integrationTitle, + }} + /> + </p> + </EuiText> + </> + ); + } else if (overallState === 'failure') { + return ( + <> + <EuiCallOut + color="danger" + title={i18n.translate( + 'xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.failureText', + { + defaultMessage: 'Agentless deployment failed', + } + )} + iconType="warning" + > + {agent?.last_checkin_message && <p>{agent.last_checkin_message}</p>} + </EuiCallOut> + <EuiSpacer size="m" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.failureHelperText" + defaultMessage="{integrationTitle} agentless integration failed to establish. Check out the {troubleshootingGuideLink} for help." + values={{ + integrationTitle, + troubleshootingGuideLink: ( + <EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank"> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.troubleshootingLinkLabel" + defaultMessage="troubleshooting guide" + /> + </EuiLink> + ), + }} + /> + </p> + </EuiText> + {agent && agentPolicy && ( + <> + <EuiSpacer size="m" /> + <AgentDetailsIntegrations agent={agent} agentPolicy={agentPolicy} linkToLogs={false} /> + </> + )} + </> + ); + } + + return ( + <> + <EuiPanel color="subdued" paddingSize="xl" className="eui-textCenter"> + <EuiButton disabled={true} size="s" isLoading={true}> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingText" + defaultMessage="Listening for agentless connection... this could take several minutes" + /> + </EuiButton> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText" + defaultMessage="Getting ready to connect with your cloud account and confirm incoming data. If you're having trouble connecting, check out the {troubleshootingGuideLink}. You can track the latest status from {policyPagePath} Status column." + values={{ + troubleshootingGuideLink: ( + <EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank"> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.troubleshootingLinkLabel" + defaultMessage="troubleshooting guide" + /> + </EuiLink> + ), + policyPagePath: ( + <strong> + <FormattedMessage + id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.policyPagePath" + defaultMessage="Integration policies → Agentless Integrations" + /> + </strong> + ), + }} + /> + </p> + </EuiText> + </> + ); +}; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/root_privileges_callout.test.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/root_privileges_callout.test.tsx index ae7e62773cb63..86cbd1a1a5a10 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/root_privileges_callout.test.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/root_privileges_callout.test.tsx @@ -13,7 +13,8 @@ import { createFleetTestRendererMock } from '../../mock'; import { RootPrivilegesCallout } from './root_privileges_callout'; -describe('RootPrivilegesCallout', () => { +// FLAKY: https://github.com/elastic/kibana/issues/201210 +describe.skip('RootPrivilegesCallout', () => { function render(rootIntegrations?: Array<{ name: string; title: string }>) { cleanup(); const renderer = createFleetTestRendererMock(); diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 5a995ab1ecbce..39dcfa3e3cd62 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -29,3 +29,4 @@ export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour'; export { UninstallCommandFlyout } from './uninstall_command_flyout'; export { MultipleAgentPoliciesSummaryLine } from './multiple_agent_policy_summary_line'; +export { AgentlessEnrollmentFlyout } from './agentless_enrollment_flyout'; diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx index f016acf8783aa..b7759438986d5 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx @@ -115,13 +115,12 @@ describe.skip('PackagePolicyActionsMenu', () => { useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: false }); }); - it('Should disable upgrade button if package does not have upgrade', async () => { + it('Should not have upgrade button if package does not have upgrade', async () => { const agentPolicies = createMockAgentPolicies(); const packagePolicy = createMockPackagePolicy({ hasUpgrade: false }); const { utils } = renderMenu({ agentPolicies, packagePolicy }); await act(async () => { - const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem'); - expect(upgradeButton).toBeDisabled(); + expect(utils.queryByTestId('PackagePolicyActionsUpgradeItem')).toBeNull(); }); }); diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index e51d61ea8ab24..25ed040f05e79 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -108,24 +108,27 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ defaultMessage="Edit integration" /> </EuiContextMenuItem>, - <EuiContextMenuItem - data-test-subj="PackagePolicyActionsUpgradeItem" - disabled={ - !packagePolicy.hasUpgrade || - !canWriteIntegrationPolicies || - !upgradePackagePolicyHref || - agentPolicy?.supports_agentless === true - } - icon="refresh" - href={upgradePackagePolicyHref} - key="packagePolicyUpgrade" - > - <FormattedMessage - id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeActionTitle" - data-test-subj="UpgradeIntegrationPolicy" - defaultMessage="Upgrade integration policy" - /> - </EuiContextMenuItem>, + ...(packagePolicy.hasUpgrade + ? [ + <EuiContextMenuItem + data-test-subj="PackagePolicyActionsUpgradeItem" + disabled={ + !canWriteIntegrationPolicies || + !upgradePackagePolicyHref || + agentPolicy?.supports_agentless === true + } + icon="refresh" + href={upgradePackagePolicyHref} + key="packagePolicyUpgrade" + > + <FormattedMessage + id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeActionTitle" + data-test-subj="UpgradeIntegrationPolicy" + defaultMessage="Upgrade integration policy" + /> + </EuiContextMenuItem>, + ] + : []), // FIXME: implement Copy package policy action // <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="packagePolicyCopy"> // <FormattedMessage diff --git a/x-pack/plugins/fleet/scripts/create_agent_policies/create_agent_policies.ts b/x-pack/plugins/fleet/scripts/create_agent_policies/create_agent_policies.ts index db47c056ce12d..e000640a477e3 100644 --- a/x-pack/plugins/fleet/scripts/create_agent_policies/create_agent_policies.ts +++ b/x-pack/plugins/fleet/scripts/create_agent_policies/create_agent_policies.ts @@ -9,8 +9,7 @@ import { ToolingLog } from '@kbn/tooling-log'; import yargs from 'yargs'; import { chunk } from 'lodash'; -import { LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../common/constants'; -import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../common/constants'; import { packagePolicyFixture } from './fixtures'; @@ -30,20 +29,18 @@ const printUsage = () => const INDEX_BULK_OP = '{ "index":{ "_id": "{{id}}" } }\n'; +const space = 'default'; function getPolicyId(idx: number | string) { - return `test-policy-${idx}`; + return `test-policy-${space}-${idx}`; } async function createAgentPoliciesDocsBulk(range: number[]) { const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64'); const body = range .flatMap((idx) => [ - INDEX_BULK_OP.replace( - /{{id}}/, - `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}:${getPolicyId(idx)}` - ), + INDEX_BULK_OP.replace(/{{id}}/, `${AGENT_POLICY_SAVED_OBJECT_TYPE}:${getPolicyId(idx)}`), JSON.stringify({ - [LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE]: { + [AGENT_POLICY_SAVED_OBJECT_TYPE]: { namespace: 'default', monitoring_enabled: ['logs', 'metrics', 'traces'], name: `Test Policy ${idx}`, @@ -60,11 +57,11 @@ async function createAgentPoliciesDocsBulk(range: number[]) { schema_version: '1.1.1', is_protected: false, }, - type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + namespaces: [space], + type: AGENT_POLICY_SAVED_OBJECT_TYPE, references: [], managed: false, coreMigrationVersion: '8.8.0', - typeMigrationVersion: '10.3.0', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) + '\n', @@ -81,7 +78,7 @@ async function createAgentPoliciesDocsBulk(range: number[]) { const data = await res.json(); if (!data.items) { - logger.error('Error creating agent policies docs: ' + JSON.stringify(data)); + logger.error('Error creating agent policy docs: ' + JSON.stringify(data)); process.exit(1); } return data; @@ -91,14 +88,14 @@ async function createEnrollmentToken(range: number[]) { const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64'); const body = range .flatMap((idx) => [ - INDEX_BULK_OP.replace(/{{id}}/, `test-enrollment-token-${idx}`), + INDEX_BULK_OP.replace(/{{id}}/, `test-enrollment-token-${space}-${idx}`), JSON.stringify({ active: true, api_key_id: 'faketest123', api_key: 'test==', name: `Test Policy ${idx}`, policy_id: `${getPolicyId(idx)}`, - namespaces: [], + namespaces: [space], created_at: new Date().toISOString(), }) + '\n', ]) @@ -115,7 +112,7 @@ async function createEnrollmentToken(range: number[]) { const data = await res.json(); if (!data.items) { - logger.error('Error creating agent policies docs: ' + JSON.stringify(data)); + logger.error('Error creating enrollment key docs: ' + JSON.stringify(data)); process.exit(1); } return data; @@ -125,14 +122,12 @@ async function createPackagePolicies(range: number[]) { const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64'); const body = range .flatMap((idx) => [ - INDEX_BULK_OP.replace( - /{{id}}/, - `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}:test-policy-${idx}` - ), + INDEX_BULK_OP.replace(/{{id}}/, `fleet-package-policies:test-policy-${space}-${idx}`), JSON.stringify( packagePolicyFixture({ idx, agentPolicyId: getPolicyId(idx), + space, }) ) + '\n', ]) @@ -150,7 +145,7 @@ async function createPackagePolicies(range: number[]) { const data = await res.json(); if (!data.items) { - logger.error('Error creating agent policies docs: ' + JSON.stringify(data)); + logger.error('Error creating package policy docs: ' + JSON.stringify(data)); process.exit(1); } return data; diff --git a/x-pack/plugins/fleet/scripts/create_agent_policies/fixtures.ts b/x-pack/plugins/fleet/scripts/create_agent_policies/fixtures.ts index b10f412ac43fe..ddaa226c0729e 100644 --- a/x-pack/plugins/fleet/scripts/create_agent_policies/fixtures.ts +++ b/x-pack/plugins/fleet/scripts/create_agent_policies/fixtures.ts @@ -8,11 +8,13 @@ export const packagePolicyFixture = ({ agentPolicyId, idx, + space, }: { idx: number; agentPolicyId: string; + space: string; }) => ({ - 'ingest-package-policies': { + 'fleet-package-policies': { name: `system-test-${idx}`, namespace: '', description: '', @@ -790,11 +792,12 @@ export const packagePolicyFixture = ({ updated_at: '2024-08-30T13:45:51.197Z', updated_by: 'system', }, - type: 'ingest-package-policies', + namespaces: [space], + type: 'fleet-package-policies', references: [], managed: false, coreMigrationVersion: '8.8.0', - typeMigrationVersion: '10.14.0', + typeMigrationVersion: '10.1.0', updated_at: '2024-08-30T13:45:51.197Z', created_at: '2024-08-30T13:45:51.197Z', }); diff --git a/x-pack/plugins/fleet/scripts/create_agent_policies/index.js b/x-pack/plugins/fleet/scripts/create_agent_policies/index.js index a51859ee684c6..a61ed0e7e54d4 100644 --- a/x-pack/plugins/fleet/scripts/create_agent_policies/index.js +++ b/x-pack/plugins/fleet/scripts/create_agent_policies/index.js @@ -12,6 +12,6 @@ require('./create_agent_policies').run(); Usage: cd x-pack/plugins/fleet -node scripts/create_agents/index.js +node scripts/create_agent_policies/index.js */ diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 8d452b394dd18..db100c5fcf7ec 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -29,6 +29,7 @@ import { createFleetActionsClientMock } from '../services/actions/mocks'; import { createFleetFilesClientFactoryMock } from '../services/files/mocks'; import { createArtifactsClientMock } from '../services/artifacts/mocks'; +import { createOutputClientMock } from '../services/output_client.mock'; import type { PackagePolicyClient } from '../services/package_policy_service'; import type { AgentPolicyServiceInterface } from '../services'; @@ -303,6 +304,7 @@ export const createFleetStartContractMock = (): DeeplyMockedKeys<FleetStartContr uninstallTokenService: createUninstallTokenServiceMock(), createFleetActionsClient: jest.fn((_) => fleetActionsClient), getPackageSpecTagId: jest.fn(getPackageSpecTagId), + createOutputClient: jest.fn(async (_) => createOutputClientMock()), }; return startContract; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4c5cdf7070530..1620df27b82c3 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -84,6 +84,8 @@ import { MessageSigningService, } from './services/security'; +import { OutputClient, type OutputClientInterface } from './services/output_client'; + import { ASSETS_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, @@ -143,6 +145,7 @@ import { registerFieldsMetadataExtractors } from './services/register_fields_met import { registerUpgradeManagedPackagePoliciesTask } from './services/setup/managed_package_policies'; import { registerDeployAgentPoliciesTask } from './services/agent_policies/deploy_agent_policies_task'; import { DeleteUnenrolledAgentsTask } from './tasks/delete_unenrolled_agents_task'; +import { registerBumpAgentPoliciesTask } from './services/agent_policies/bump_agent_policies_task'; export interface FleetSetupDeps { security: SecurityPluginSetup; @@ -261,6 +264,12 @@ export interface FleetStartContract { Function exported to allow creating unique ids for saved object tags */ getPackageSpecTagId: (spaceId: string, pkgName: string, tagName: string) => string; + + /** + * Create a Fleet Output Client instance + * @param packageName + */ + createOutputClient: (request: KibanaRequest) => Promise<OutputClientInterface>; } export class FleetPlugin @@ -619,6 +628,7 @@ export class FleetPlugin // Register task registerUpgradeManagedPackagePoliciesTask(deps.taskManager); registerDeployAgentPoliciesTask(deps.taskManager); + registerBumpAgentPoliciesTask(deps.taskManager); this.bulkActionsResolver = new BulkActionsResolver(deps.taskManager, core); this.checkDeletedFilesTask = new CheckDeletedFilesTask({ @@ -835,6 +845,11 @@ export class FleetPlugin return new FleetActionsClient(core.elasticsearch.client.asInternalUser, packageName); }, getPackageSpecTagId, + async createOutputClient(request: KibanaRequest) { + const soClient = appContextService.getSavedObjects().getScopedClient(request); + const authz = await getAuthzFromRequest(request); + return new OutputClient(soClient, authz); + }, }; } diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index bcefa56b806e8..c51fb9de674c7 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -46,6 +46,8 @@ import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics' import { getAgentStatusForAgentPolicy } from '../../services/agents'; import { isAgentInNamespace } from '../../services/spaces/agent_namespaces'; import { getCurrentNamespace } from '../../services/spaces/get_current_namespace'; +import { getPackageInfo } from '../../services/epm/packages'; +import { generateTemplateIndexPattern } from '../../services/epm/elasticsearch/template/template'; import { buildAgentStatusRuntimeField } from '../../services/agents/build_status_runtime_field'; async function verifyNamespace(agent: Agent, namespace?: string) { @@ -308,17 +310,32 @@ export const getAgentDataHandler: RequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asCurrentUser; - - const returnDataPreview = request.query.previewData; - const agentIds = isStringArray(request.query.agentsIds) + const agentsIds = isStringArray(request.query.agentsIds) ? request.query.agentsIds : [request.query.agentsIds]; + const { pkgName, pkgVersion, previewData: returnDataPreview } = request.query; + + // If a package is specified, get data stream patterns for that package + // and scope incoming data query to that pattern + let dataStreamPattern: string | undefined; + if (pkgName && pkgVersion) { + const packageInfo = await getPackageInfo({ + savedObjectsClient: coreContext.savedObjects.client, + prerelease: true, + pkgName, + pkgVersion, + }); + dataStreamPattern = (packageInfo.data_streams || []) + .map((ds) => generateTemplateIndexPattern(ds)) + .join(','); + } - const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId( + const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId({ esClient, - agentIds, - returnDataPreview - ); + agentsIds, + dataStreamPattern, + returnDataPreview, + }); const body = { items, dataPreview }; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts index 518d8fc3f74c8..a174cb65fe3ce 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts @@ -11,7 +11,7 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { isAgentlessApiEnabled } from '../../../services/utils/agentless'; +import { isAgentlessEnabled } from '../../../services/utils/agentless'; import { getAgentlessAgentPolicyNameFromPackagePolicyName } from '../../../../common/services/agentless_policy_helper'; @@ -65,7 +65,7 @@ export async function renameAgentlessAgentPolicy( packagePolicy: PackagePolicy, name: string ) { - if (!isAgentlessApiEnabled()) { + if (!isAgentlessEnabled()) { return; } // If agentless is enabled for cloud, we need to rename the agent policy diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 706d0686b9845..0f196686a4719 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -614,11 +614,13 @@ export const getSavedObjectTypes = ( }, secret_references: { properties: { id: { type: 'keyword' } } }, overrides: { type: 'flattened', index: false }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, created_at: { type: 'date' }, created_by: { type: 'keyword' }, + bump_agent_policy_revision: { type: 'boolean' }, }, }, modelVersions: { @@ -763,6 +765,26 @@ export const getSavedObjectTypes = ( }, ], }, + '15': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + bump_agent_policy_revision: { type: 'boolean' }, + }, + }, + ], + }, + '16': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + supports_agentless: { type: 'boolean' }, + }, + }, + ], + }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, @@ -818,11 +840,35 @@ export const getSavedObjectTypes = ( }, secret_references: { properties: { id: { type: 'keyword' } } }, overrides: { type: 'flattened', index: false }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, created_at: { type: 'date' }, created_by: { type: 'keyword' }, + bump_agent_policy_revision: { type: 'boolean' }, + }, + }, + modelVersions: { + '1': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + bump_agent_policy_revision: { type: 'boolean' }, + }, + }, + ], + }, + '2': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + supports_agentless: { type: 'boolean' }, + }, + }, + ], }, }, }, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.test.ts new file mode 100644 index 0000000000000..2e7305c5c5717 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +import { agentPolicyService } from '../agent_policy'; + +import { appContextService } from '..'; +import { getPackagePolicySavedObjectType } from '../package_policy'; + +import { _updatePackagePoliciesThatNeedBump } from './bump_agent_policies_task'; + +jest.mock('../app_context'); +jest.mock('../agent_policy'); +jest.mock('../package_policy'); + +const mockedAgentPolicyService = jest.mocked(agentPolicyService); +const mockedAppContextService = jest.mocked(appContextService); +const mockSoClient = { + find: jest.fn(), + bulkUpdate: jest.fn(), +} as any; +const mockGetPackagePolicySavedObjectType = jest.mocked(getPackagePolicySavedObjectType); + +describe('_updatePackagePoliciesThatNeedBump', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockSoClient.find.mockResolvedValue({ + total: 3, + saved_objects: [ + { + id: 'packagePolicy1', + namespaces: ['default'], + attributes: { + policy_ids: ['policy1'], + }, + }, + { + id: 'packagePolicy12', + namespaces: ['default'], + attributes: { + policy_ids: ['policy1'], + }, + }, + { + id: 'packagePolicy2', + namespaces: ['space'], + attributes: { + policy_ids: ['policy2'], + }, + }, + { + id: 'packagePolicy3', + namespaces: ['space'], + attributes: { + policy_ids: ['policy3'], + }, + }, + ], + page: 1, + perPage: 100, + }); + mockedAppContextService.getInternalUserSOClientWithoutSpaceExtension.mockReturnValue( + mockSoClient + ); + mockedAppContextService.getInternalUserSOClientForSpaceId.mockReturnValue(mockSoClient); + mockGetPackagePolicySavedObjectType.mockResolvedValue('fleet-package-policies'); + }); + + it('should update package policy if bump agent policy revision needed', async () => { + const logger = loggingSystemMock.createLogger(); + + await _updatePackagePoliciesThatNeedBump(logger, () => false); + + expect(mockSoClient.bulkUpdate).toHaveBeenCalledWith([ + { + attributes: { bump_agent_policy_revision: false }, + id: 'packagePolicy1', + type: 'fleet-package-policies', + }, + { + attributes: { bump_agent_policy_revision: false }, + id: 'packagePolicy12', + type: 'fleet-package-policies', + }, + ]); + expect(mockSoClient.bulkUpdate).toHaveBeenCalledWith([ + { + attributes: { bump_agent_policy_revision: false }, + id: 'packagePolicy2', + type: 'fleet-package-policies', + }, + { + attributes: { bump_agent_policy_revision: false }, + id: 'packagePolicy3', + type: 'fleet-package-policies', + }, + ]); + + expect(mockedAgentPolicyService.bumpAgentPoliciesByIds).toHaveBeenCalledWith( + expect.anything(), + undefined, + ['policy1'] + ); + expect(mockedAgentPolicyService.bumpAgentPoliciesByIds).toHaveBeenCalledWith( + expect.anything(), + undefined, + ['policy2', 'policy3'] + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.ts b/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.ts new file mode 100644 index 0000000000000..d134fdbfae04f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/bump_agent_policies_task.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 { Logger } from '@kbn/core/server'; +import type { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import { v4 as uuidv4 } from 'uuid'; +import { uniq } from 'lodash'; + +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; + +import { agentPolicyService, appContextService } from '..'; +import { runWithCache } from '../epm/packages/cache'; +import { getPackagePolicySavedObjectType } from '../package_policy'; +import { mapPackagePolicySavedObjectToPackagePolicy } from '../package_policies'; +import type { PackagePolicy, PackagePolicySOAttributes } from '../../types'; +import { normalizeKuery } from '../saved_object'; +import { SO_SEARCH_LIMIT } from '../../constants'; + +const TASK_TYPE = 'fleet:bump_agent_policies'; + +export function registerBumpAgentPoliciesTask(taskManagerSetup: TaskManagerSetupContract) { + taskManagerSetup.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Fleet Bump policies', + timeout: '5m', + maxAttempts: 3, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + let cancelled = false; + const isCancelled = () => cancelled; + return { + async run() { + if (isCancelled()) { + throw new Error('Task has been cancelled'); + } + + await runWithCache(async () => { + await _updatePackagePoliciesThatNeedBump(appContextService.getLogger(), isCancelled); + }); + }, + async cancel() { + cancelled = true; + }, + }; + }, + }, + }); +} + +async function getPackagePoliciesToBump(savedObjectType: string) { + const result = await appContextService + .getInternalUserSOClientWithoutSpaceExtension() + .find<PackagePolicySOAttributes>({ + type: savedObjectType, + filter: normalizeKuery(savedObjectType, `${savedObjectType}.bump_agent_policy_revision:true`), + perPage: SO_SEARCH_LIMIT, + namespaces: ['*'], + fields: ['id', 'namespaces', 'policy_ids'], + }); + return { + total: result.total, + items: result.saved_objects.map((so) => + mapPackagePolicySavedObjectToPackagePolicy(so, so.namespaces) + ), + }; +} + +export async function _updatePackagePoliciesThatNeedBump( + logger: Logger, + isCancelled: () => boolean +) { + const savedObjectType = await getPackagePolicySavedObjectType(); + const packagePoliciesToBump = await getPackagePoliciesToBump(savedObjectType); + + logger.info( + `Found ${packagePoliciesToBump.total} package policies that need agent policy revision bump` + ); + + const packagePoliciesIndexedBySpace = packagePoliciesToBump.items.reduce((acc, policy) => { + const spaceId = policy.spaceIds?.[0] ?? DEFAULT_SPACE_ID; + if (!acc[spaceId]) { + acc[spaceId] = []; + } + + acc[spaceId].push(policy); + + return acc; + }, {} as { [k: string]: PackagePolicy[] }); + + const start = Date.now(); + + for (const [spaceId, packagePolicies] of Object.entries(packagePoliciesIndexedBySpace)) { + if (isCancelled()) { + throw new Error('Task has been cancelled'); + } + + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); + const esClient = appContextService.getInternalUserESClient(); + + await soClient.bulkUpdate<PackagePolicySOAttributes>( + packagePolicies.map((item) => ({ + type: savedObjectType, + id: item.id, + attributes: { + bump_agent_policy_revision: false, + }, + })) + ); + + const updatedCount = packagePolicies.length; + + const agentPoliciesToBump = uniq(packagePolicies.map((item) => item.policy_ids).flat()); + + await agentPolicyService.bumpAgentPoliciesByIds(soClient, esClient, agentPoliciesToBump); + + logger.debug( + `Updated ${updatedCount} package policies in space ${spaceId} in ${ + Date.now() - start + }ms, bump ${agentPoliciesToBump.length} agent policies` + ); + } +} + +export async function scheduleBumpAgentPoliciesTask(taskManagerStart: TaskManagerStartContract) { + await taskManagerStart.ensureScheduled({ + id: `${TASK_TYPE}:${uuidv4()}`, + scope: ['fleet'], + params: {}, + taskType: TASK_TYPE, + runAt: new Date(Date.now() + 3 * 1000), + state: {}, + }); +} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index fb3274b6eef77..76d2727b65e85 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -157,7 +157,7 @@ describe('Agent policy', () => { beforeEach(() => { mockedLogger = loggerMock.create(); mockedAppContextService.getLogger.mockReturnValue(mockedLogger); - mockedAppContextService.getExperimentalFeatures.mockReturnValue({ agentless: false } as any); + mockedAppContextService.getExperimentalFeatures.mockReturnValue({} as any); jest.mocked(isSpaceAwarenessEnabled).mockResolvedValue(false); jest .mocked(getPackagePolicySavedObjectType) @@ -315,10 +315,7 @@ describe('Agent policy', () => { ); }); - it('should throw AgentPolicyInvalidError if support_agentless is defined in stateful without agentless feature', async () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); + it('should throw AgentPolicyInvalidError if support_agentless is defined in stateful without agentless enabled', async () => { jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: false } as any); @@ -339,10 +336,7 @@ describe('Agent policy', () => { ); }); - it('should throw AgentPolicyInvalidError if agentless feature flag is disabled in serverless', async () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); + it('should throw AgentPolicyInvalidError if agentless is disabled in serverless', async () => { jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: true } as any); @@ -363,10 +357,10 @@ describe('Agent policy', () => { ); }); - it('should create a policy agentless feature flag is set and in serverless env', async () => { + it('should create an agentless policy when agentless config is set and in serverless env', async () => { jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); + .spyOn(appContextService, 'getConfig') + .mockReturnValue({ agentless: { enabled: true } } as any); jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: true } as any); @@ -1229,13 +1223,13 @@ describe('Agent policy', () => { ).resolves.not.toThrow(); }); - it('should throw AgentPolicyInvalidError if agentless flag is disabled in serverless', async () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); + it('should not throw AgentPolicyInvalidError if support_agentless is defined in serverless', async () => { + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { enabled: true }, + } as any); jest .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isServerlessEnabled: true } as any); + .mockReturnValue({ isServerlessEnabled: true, isCloudEnabled: false } as any); const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -1252,17 +1246,13 @@ describe('Agent policy', () => { namespace: 'default', supports_agentless: true, }) - ).rejects.toThrowError( - new AgentPolicyInvalidError( - 'supports_agentless is only allowed in serverless and cloud environments that support the agentless feature' - ) - ); + ).resolves.not.toThrow(); }); - it('should not throw in serverless if support_agentless is set and agentless feature flag is set', async () => { + it('should not throw in serverless if support_agentless and agentless config is set', async () => { jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); + .spyOn(appContextService, 'getConfig') + .mockReturnValue({ agentless: { enabled: true } } as any); jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: true } as any); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index cada1c8e64452..21614d2a97481 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -1646,6 +1646,27 @@ class AgentPolicyService { ); } + public async bumpAgentPoliciesByIds( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentPolicyIds: string[], + options?: { user?: AuthenticatedUser } + ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { + const internalSoClientWithoutSpaceExtension = + appContextService.getInternalUserSOClientWithoutSpaceExtension(); + const savedObjectType = await getAgentPolicySavedObjectType(); + + const objects = agentPolicyIds.map((id: string) => ({ id, type: savedObjectType })); + const bulkGetResponse = await soClient.bulkGet<AgentPolicySOAttributes>(objects); + + return this._bumpPolicies( + internalSoClientWithoutSpaceExtension, + esClient, + bulkGetResponse.saved_objects, + options + ); + } + public async getInactivityTimeouts(): Promise< Array<{ policyIds: string[]; inactivityTimeout: number }> > { diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 314af0ada7bf4..6d1945fced809 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -35,7 +35,7 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; import type { AgentlessConfig } from '../utils/agentless'; -import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; +import { prependAgentlessApiBasePathToEndpoint, isAgentlessEnabled } from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -59,7 +59,7 @@ class AgentlessAgentService { throw new AgentlessAgentConfigError('missing Agentless API configuration in Kibana'); } - if (!isAgentlessApiEnabled()) { + if (!isAgentlessEnabled()) { logger.error( '[Agentless API] Agentless agents are only supported in cloud deployment and serverless projects' ); @@ -179,7 +179,7 @@ class AgentlessAgentService { `[Agentless API] Start deleting agentless agent for agent policy ${requestConfigDebugStatus}` ); - if (!isAgentlessApiEnabled) { + if (!isAgentlessEnabled) { logger.error( '[Agentless API] Agentless API is not supported. Deleting agentless agent is not supported in non-cloud or non-serverless environments' ); @@ -431,45 +431,38 @@ class AgentlessAgentService { 400: { create: { log: '[Agentless API] Creating the agentless agent failed with a status 400, bad request for agentless policy.', - message: - 'the Agentless API could not create the agentless agent. Please delete the agentless policy and try again or contact your administrator.', + message: `the Agentless API could not create the agentless agent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 400, bad request for agentless policy', - message: - 'the Agentless API could not create the agentless agent. Please delete the agentless policy try again or contact your administrator.', + message: `the Agentless API could not create the agentless agent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, 401: { create: { log: '[Agentless API] Creating the agentless agent failed with a status 401 unauthorized for agentless policy.', - message: - 'the Agentless API could not create the agentless agent because an unauthorized request was sent. Please delete the agentless policy and try again or contact your administrator.', + message: `the Agentless API could not create the agentless agent because an unauthorized request was sent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 401 unauthorized for agentless policy. Check the Kibana Agentless API tls configuration', - message: - 'the Agentless API could not delete the agentless deployment because an unauthorized request was sent. Please try again or contact your administrator.', + message: `the Agentless API could not delete the agentless deployment because an unauthorized request was sent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, 403: { create: { log: '[Agentless API] Creating the agentless agent failed with a status 403 forbidden for agentless policy. Check the Kibana Agentless API configuration and endpoints.', - message: - 'the Agentless API could not create the agentless agent because a forbidden request was sent. Please delete the agentless policy and try again or contact your administrator.', + message: `the Agentless API could not create the agentless agent because a forbidden request was sent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 403 forbidden for agentless policy. Check the Kibana Agentless API configuration and endpoints.', - message: - 'the Agentless API could not delete the agentless deployment because a forbidden request was sent. Please try again or contact your administrator.', + message: `the Agentless API could not delete the agentless deployment because a forbidden request was sent. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, 404: { // this is likely to happen when creating agentless agents, but covering it in case create: { log: '[Agentless API] Creating the agentless agent failed with a status 404 not found.', - message: - 'the Agentless API could not create the agentless agent because it returned a 404 error not found.', + message: `the Agentless API could not create the agentless agent because it returned a 404 error not found. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 404 not found', @@ -479,12 +472,11 @@ class AgentlessAgentService { 408: { create: { log: '[Agentless API] Creating the agentless agent failed with a status 408, the request timed out', - message: - 'the Agentless API request timed out waiting for the agentless agent status to respond, please wait a few minutes for the agent to enroll with fleet. If agent fails to enroll with Fleet please delete the agentless policy try again or contact your administrator.', + message: `the Agentless API request timed out waiting for the agentless agent status to respond, please wait a few minutes for the agent to enroll with fleet. If agent fails to enroll with Fleet please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 408, the request timed out', - message: `the Agentless API could not delete the agentless deployment ${agentlessPolicyId} because the request timed out, please wait a few minutes for the agentless agent deployment to be removed. If it continues to persist please try again or contact your administrator.`, + message: `the Agentless API could not delete the agentless deployment because the request timed out, please wait a few minutes for the agentless agent deployment to be removed. If it continues to persist please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, 429: { @@ -503,36 +495,31 @@ class AgentlessAgentService { 500: { create: { log: '[Agentless API] Creating the agentless agent failed with a status 500 internal service error.', - message: - 'the Agentless API could not create the agentless agent because it returned a 500 internal error. Please delete the agentless policy and try again later or contact your administrator.', + message: `the Agentless API could not create the agentless agent because it returned a 500 internal error. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting the agentless deployment failed with a status 500 internal service error.', - message: - 'the Agentless API could not delete the agentless deployment because it returned a 500 internal error. Please try again later or contact your administrator.', + message: `the Agentless API could not delete the agentless deployment because it returned a 500 internal error. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, unhandled_response: { create: { log: '[Agentless API] Creating agentless agent failed because the Agentless API responded with an unhandled status code that falls out of the range of 2xx:', - message: - 'the Agentless API could not create the agentless agent due to an unexpected error. Please delete the agentless policy and try again later or contact your administrator.', + message: `the Agentless API could not create the agentless agent due to an unexpected error. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting agentless deployment failed because the Agentless API responded with an unhandled status code that falls out of the range of 2xx:', - message: `the Agentless API could not delete the agentless deployment ${agentlessPolicyId}. Please try again later or contact your administrator.`, + message: `the Agentless API could not delete the agentless deployment. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, request_error: { create: { log: '[Agentless API] Creating agentless agent failed with a request error:', - message: - 'the Agentless API could not create the agentless agent due to a request error. Please delete the agentless policy and try again later or contact your administrator.', + message: `the Agentless API could not create the agentless agent due to a request error. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, delete: { log: '[Agentless API] Deleting agentless deployment failed with a request error:', - message: - 'the Agentless API could not delete the agentless deployment due to a request error. Please try again later or contact your administrator.', + message: `the Agentless API could not delete the agentless deployment due to a request error. Please delete the agentless policy ${agentlessPolicyId} and try again or contact your administrator.`, }, }, }; diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c413ee7e268c8..e960a7b955fea 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -177,11 +177,17 @@ export async function getAgentStatusForAgentPolicy( }; } -export async function getIncomingDataByAgentsId( - esClient: ElasticsearchClient, - agentsIds: string[], - returnDataPreview: boolean = false -) { +export async function getIncomingDataByAgentsId({ + esClient, + agentsIds, + dataStreamPattern = DATA_STREAM_INDEX_PATTERN, + returnDataPreview = false, +}: { + esClient: ElasticsearchClient; + agentsIds: string[]; + dataStreamPattern?: string; + returnDataPreview?: boolean; +}) { const logger = appContextService.getLogger(); try { @@ -189,7 +195,7 @@ export async function getIncomingDataByAgentsId( body: { index: [ { - names: [DATA_STREAM_INDEX_PATTERN], + names: [dataStreamPattern], privileges: ['read'], }, ], @@ -203,7 +209,7 @@ export async function getIncomingDataByAgentsId( const searchResult = await retryTransientEsErrors( () => esClient.search({ - index: DATA_STREAM_INDEX_PATTERN, + index: dataStreamPattern, allow_partial_search_results: true, _source: returnDataPreview, timeout: '5s', @@ -244,9 +250,9 @@ export async function getIncomingDataByAgentsId( if (!searchResult.aggregations?.agent_ids) { return { items: agentsIds.map((id) => { - return { items: { [id]: { data: false } } }; + return { [id]: { data: false } }; }), - data: [], + dataPreview: [], }; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index c06d0cdbb6429..dbc93e35c8218 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -1900,9 +1900,19 @@ describe('EPM template', () => { it('should fill constant keywords from previous mappings', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.indices.getDataStream.mockResponse({ - data_streams: [{ name: 'test-constant.keyword-default' }], + data_streams: [ + { + name: 'test-constant.keyword-default', + indices: [ + { index_name: '.ds-test-constant.keyword-default-0001' }, + { index_name: '.ds-test-constant.keyword-default-0002' }, + ], + }, + ], } as any); + esClient.indices.get.mockResponse({ 'test-constant.keyword-default': { mappings: { @@ -1942,6 +1952,9 @@ describe('EPM template', () => { } as any, }, ]); + expect(esClient.indices.get).toBeCalledWith({ + index: '.ds-test-constant.keyword-default-0002', + }); const putMappingsCalls = esClient.indices.putMapping.mock.calls; expect(putMappingsCalls).toHaveLength(1); expect(putMappingsCalls[0][0]).toEqual({ diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index b9c0846f3e4f2..bb38480fd66b7 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -65,6 +65,7 @@ export interface CurrentDataStream { dataStreamName: string; replicated: boolean; indexTemplate: IndexTemplate; + currentWriteIndex: string; } const DEFAULT_IGNORE_ABOVE = 1024; @@ -954,6 +955,7 @@ const getDataStreams = async ( dataStreamName: dataStream.name, replicated: dataStream.replicated, indexTemplate, + currentWriteIndex: dataStream.indices?.at(-1)?.index_name, })); }; @@ -989,6 +991,7 @@ const updateAllDataStreams = async ( return updateExistingDataStream({ esClient, logger, + currentWriteIndex: templateEntry.currentWriteIndex, dataStreamName: templateEntry.dataStreamName, options, }); @@ -1002,11 +1005,13 @@ const updateAllDataStreams = async ( const updateExistingDataStream = async ({ dataStreamName, + currentWriteIndex, esClient, logger, options, }: { dataStreamName: string; + currentWriteIndex: string; esClient: ElasticsearchClient; logger: Logger; options?: { @@ -1015,7 +1020,7 @@ const updateExistingDataStream = async ({ }; }) => { const existingDs = await esClient.indices.get({ - index: dataStreamName, + index: currentWriteIndex, }); const existingDsConfig = Object.values(existingDs); diff --git a/x-pack/plugins/fleet/server/services/output_client.mock.ts b/x-pack/plugins/fleet/server/services/output_client.mock.ts new file mode 100644 index 0000000000000..ba684a2aff615 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/output_client.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { OutputClientInterface } from './output_client'; + +export const createOutputClientMock = (): jest.Mocked<OutputClientInterface> => { + return { + getDefaultDataOutputId: jest.fn(), + get: jest.fn(), + }; +}; diff --git a/x-pack/plugins/fleet/server/services/output_client.test.ts b/x-pack/plugins/fleet/server/services/output_client.test.ts new file mode 100644 index 0000000000000..f3d4b83bea4bc --- /dev/null +++ b/x-pack/plugins/fleet/server/services/output_client.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; + +import { createFleetAuthzMock } from '../../common/mocks'; + +import { OutputClient } from './output_client'; +import { outputService } from './output'; + +jest.mock('./output'); + +const mockedOutputService = outputService as jest.Mocked<typeof outputService>; + +describe('OutputClient', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getDefaultDataOutputId()', () => { + it('should call output service `getDefaultDataOutputId()` method', async () => { + const soClient = savedObjectsClientMock.create(); + const authz = createFleetAuthzMock(); + const outputClient = new OutputClient(soClient, authz); + await outputClient.getDefaultDataOutputId(); + + expect(mockedOutputService.getDefaultDataOutputId).toHaveBeenCalledWith(soClient); + }); + + it('should throw error when no `fleet.readSettings` and no `fleet.readAgentPolicies` privileges', async () => { + const soClient = savedObjectsClientMock.create(); + const authz = createFleetAuthzMock(); + authz.fleet.readSettings = false; + authz.fleet.readAgentPolicies = false; + const outputClient = new OutputClient(soClient, authz); + + await expect(outputClient.getDefaultDataOutputId()).rejects.toMatchInlineSnapshot( + `[OutputUnauthorizedError]` + ); + expect(mockedOutputService.getDefaultDataOutputId).not.toHaveBeenCalled(); + }); + }); + + describe('get()', () => { + it('should call output service `get()` method', async () => { + const soClient = savedObjectsClientMock.create(); + const authz = createFleetAuthzMock(); + const outputClient = new OutputClient(soClient, authz); + await outputClient.get('default'); + + expect(mockedOutputService.get).toHaveBeenCalledWith(soClient, 'default'); + }); + + it('should throw error when no `fleet.readSettings` and no `fleet.readAgentPolicies` privileges', async () => { + const soClient = savedObjectsClientMock.create(); + const authz = createFleetAuthzMock(); + authz.fleet.readSettings = false; + authz.fleet.readAgentPolicies = false; + const outputClient = new OutputClient(soClient, authz); + + await expect(outputClient.get('default')).rejects.toMatchInlineSnapshot( + `[OutputUnauthorizedError]` + ); + expect(mockedOutputService.get).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/output_client.ts b/x-pack/plugins/fleet/server/services/output_client.ts new file mode 100644 index 0000000000000..574446e1f32a7 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/output_client.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 type { SavedObjectsClientContract } from '@kbn/core/server'; + +import type { FleetAuthz } from '../../common'; + +import { OutputUnauthorizedError } from '../errors'; +import type { Output } from '../types'; + +import { outputService } from './output'; + +export { transformOutputToFullPolicyOutput } from './agent_policies/full_agent_policy'; + +export interface OutputClientInterface { + getDefaultDataOutputId(): Promise<string | null>; + get(outputId: string): Promise<Output>; +} + +export class OutputClient implements OutputClientInterface { + constructor(private soClient: SavedObjectsClientContract, private authz: FleetAuthz) {} + + async getDefaultDataOutputId() { + if (!this.authz.fleet.readSettings && !this.authz.fleet.readAgentPolicies) { + throw new OutputUnauthorizedError(); + } + return outputService.getDefaultDataOutputId(this.soClient); + } + + async get(outputId: string) { + if (!this.authz.fleet.readSettings && !this.authz.fleet.readAgentPolicies) { + throw new OutputUnauthorizedError(); + } + return outputService.get(this.soClient, outputId); + } +} diff --git a/x-pack/plugins/fleet/server/services/package_policies/utils.ts b/x-pack/plugins/fleet/server/services/package_policies/utils.ts index ef59c643a8b35..a27ac5943f80f 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/utils.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/utils.ts @@ -28,17 +28,16 @@ import { licenseService } from '../license'; import { outputService } from '../output'; import { appContextService } from '../app_context'; -export const mapPackagePolicySavedObjectToPackagePolicy = ({ - id, - version, - attributes, - namespaces, -}: SavedObject<PackagePolicySOAttributes>): PackagePolicy => { +export const mapPackagePolicySavedObjectToPackagePolicy = ( + { id, version, attributes }: SavedObject<PackagePolicySOAttributes>, + namespaces?: string[] +): PackagePolicy => { + const { bump_agent_policy_revision: bumpAgentPolicyRevision, ...restAttributes } = attributes; return { id, version, - spaceIds: namespaces, - ...attributes, + ...(namespaces ? { spaceIds: namespaces } : {}), + ...restAttributes, }; }; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 48dc3956d6984..eac31bb980256 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -20,6 +20,7 @@ import type { RequestHandlerContext, SavedObjectsBulkCreateObject, SavedObjectsBulkUpdateObject, + SavedObject, } from '@kbn/core/server'; import { SavedObjectsUtils } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; @@ -446,7 +447,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } } - const createdPackagePolicy = { id: newSo.id, version: newSo.version, ...newSo.attributes }; + const createdPackagePolicy = mapPackagePolicySavedObjectToPackagePolicy(newSo); logger.debug(`Created new package policy with id ${newSo.id} and version ${newSo.version}`); return packagePolicyService.runExternalCallbacks( @@ -668,11 +669,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } logger.debug(`Created new package policies`); return { - created: newSos.map((newSo) => ({ - id: newSo.id, - version: newSo.version, - ...newSo.attributes, - })), + created: newSos.map((newSo) => mapPackagePolicySavedObjectToPackagePolicy(newSo)), failed: failedPolicies, }; } @@ -754,11 +751,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } } - const response = { - id: packagePolicySO.id, - version: packagePolicySO.version, - ...packagePolicySO.attributes, - }; + const response = mapPackagePolicySavedObjectToPackagePolicy(packagePolicySO); // If possible, return the experimental features map for the package policy's `package` field if (experimentalFeatures && response.package) { @@ -788,11 +781,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { return []; } - const packagePolicies = packagePolicySO.saved_objects.map((so) => ({ - id: so.id, - version: so.version, - ...so.attributes, - })); + const packagePolicies = packagePolicySO.saved_objects.map((so) => + mapPackagePolicySavedObjectToPackagePolicy(so) + ); for (const packagePolicy of packagePolicies) { auditLoggingService.writeCustomSoAuditLog({ @@ -835,11 +826,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } } - return { - id: so.id, - version: so.version, - ...so.attributes, - }; + return mapPackagePolicySavedObjectToPackagePolicy(so); }) .filter((packagePolicy): packagePolicy is PackagePolicy => packagePolicy !== null); @@ -889,12 +876,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } return { - items: packagePolicies?.saved_objects.map((packagePolicySO) => ({ - id: packagePolicySO.id, - version: packagePolicySO.version, - ...packagePolicySO.attributes, - spaceIds: packagePolicySO.namespaces, - })), + items: packagePolicies?.saved_objects.map((so) => + mapPackagePolicySavedObjectToPackagePolicy(so, so.namespaces) + ), total: packagePolicies?.total, page, perPage, @@ -1392,13 +1376,10 @@ class PackagePolicyClientImpl implements PackagePolicyClient { const updatedPoliciesSuccess = updatedPolicies .filter((policy) => !policy.error && policy.attributes) - .map( - (soPolicy) => - ({ - id: soPolicy.id, - version: soPolicy.version, - ...soPolicy.attributes, - } as PackagePolicy) + .map((soPolicy) => + mapPackagePolicySavedObjectToPackagePolicy( + soPolicy as SavedObject<PackagePolicySOAttributes> + ) ); return { updatedPolicies: updatedPoliciesSuccess, failedPolicies }; @@ -1948,6 +1929,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { output_id: newPolicy.output_id, inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs, vars: newPolicy.vars || newPP.vars, + supports_agentless: newPolicy.supports_agentless, }; } } @@ -2189,7 +2171,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }) - ).saved_objects.map(mapPackagePolicySavedObjectToPackagePolicy); + ).saved_objects.map((so) => mapPackagePolicySavedObjectToPackagePolicy(so, so.namespaces)); if (packagePolicies.length > 0) { const getPackagePolicyUpdate = (packagePolicy: PackagePolicy) => ({ @@ -2306,7 +2288,10 @@ class PackagePolicyClientImpl implements PackagePolicyClient { savedObjectType, }); - return mapPackagePolicySavedObjectToPackagePolicy(packagePolicySO); + return mapPackagePolicySavedObjectToPackagePolicy( + packagePolicySO, + packagePolicySO.namespaces + ); }); }, }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index fb2153ff903f0..57621238d914f 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -306,10 +306,8 @@ jest.mock('./app_context', () => ({ }), getExternalCallbacks: jest.fn(), getCloud: jest.fn(), - getExperimentalFeatures: jest.fn().mockReturnValue({ - agentless: false, - }), getConfig: jest.fn(), + getExperimentalFeatures: jest.fn().mockReturnValue({}), getInternalUserSOClientForSpaceId: jest.fn(), }, })); @@ -1075,10 +1073,6 @@ describe('policy preconfiguration', () => { DEFAULT_SPACE_ID ); - jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({ - agentless: true, - } as any); - expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledTimes(1); expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith(TEST_NAMESPACE); diff --git a/x-pack/plugins/fleet/server/services/settings.test.ts b/x-pack/plugins/fleet/server/services/settings.test.ts index f88e735dfcb69..9a75dc72abf3e 100644 --- a/x-pack/plugins/fleet/server/services/settings.test.ts +++ b/x-pack/plugins/fleet/server/services/settings.test.ts @@ -143,6 +143,42 @@ describe('getSettings', () => { await getSettings(soClient); }); + + it('should handle null values for space awareness migration fields', async () => { + const soClient = savedObjectsClientMock.create(); + + soClient.find.mockResolvedValueOnce({ + saved_objects: [ + { + id: GLOBAL_SETTINGS_ID, + attributes: { + use_space_awareness_migration_status: null, + use_space_awareness_migration_started_at: null, + }, + references: [], + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + score: 0, + }, + ], + page: 1, + per_page: 10, + total: 1, + }); + + const settings = await getSettings(soClient); + expect(settings).toEqual({ + delete_unenrolled_agents: undefined, + has_seen_add_data_notice: undefined, + id: 'fleet-default-settings', + output_secret_storage_requirements_met: undefined, + preconfigured_fields: [], + prerelease_integrations_enabled: undefined, + secret_storage_requirements_met: undefined, + use_space_awareness_migration_started_at: undefined, + use_space_awareness_migration_status: undefined, + version: undefined, + }); + }); }); describe('saveSettings', () => { diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 3288ec1090e41..5e1948d5d3797 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -6,7 +6,11 @@ */ import Boom from '@hapi/boom'; -import type { SavedObjectsClientContract, SavedObjectsUpdateOptions } from '@kbn/core/server'; +import type { + SavedObject, + SavedObjectsClientContract, + SavedObjectsUpdateOptions, +} from '@kbn/core/server'; import { omit } from 'lodash'; import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_ID } from '../../common/constants'; @@ -18,21 +22,7 @@ import { DeleteUnenrolledAgentsPreconfiguredError } from '../errors'; import { appContextService } from './app_context'; import { auditLoggingService } from './audit_logging'; -export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> { - const res = await soClient.find<SettingsSOAttributes>({ - type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, - }); - auditLoggingService.writeCustomSoAuditLog({ - action: 'get', - id: GLOBAL_SETTINGS_ID, - savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, - }); - - if (res.total === 0) { - throw Boom.notFound('Global settings not found'); - } - const settingsSo = res.saved_objects[0]; - +function mapSettingsSO(settingsSo: SavedObject<SettingsSOAttributes>): Settings { return { id: settingsSo.id, version: settingsSo.version, @@ -42,14 +32,30 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise has_seen_add_data_notice: settingsSo.attributes.has_seen_add_data_notice, prerelease_integrations_enabled: settingsSo.attributes.prerelease_integrations_enabled, use_space_awareness_migration_status: - settingsSo.attributes.use_space_awareness_migration_status, + settingsSo.attributes.use_space_awareness_migration_status ?? undefined, use_space_awareness_migration_started_at: - settingsSo.attributes.use_space_awareness_migration_started_at, + settingsSo.attributes.use_space_awareness_migration_started_at ?? undefined, preconfigured_fields: getConfigFleetServerHosts() ? ['fleet_server_hosts'] : [], delete_unenrolled_agents: settingsSo.attributes.delete_unenrolled_agents, }; } +export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> { + const res = await soClient.find<SettingsSOAttributes>({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + }); + auditLoggingService.writeCustomSoAuditLog({ + action: 'get', + id: GLOBAL_SETTINGS_ID, + savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + }); + + if (res.total === 0) { + throw Boom.notFound('Global settings not found'); + } + return mapSettingsSO(res.saved_objects[0]); +} + export async function getSettingsOrUndefined( soClient: SavedObjectsClientContract ): Promise<Settings | undefined> { diff --git a/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.test.ts b/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.test.ts index 07ec6593ec9e5..9fb30ad75ab7e 100644 --- a/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.test.ts +++ b/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.test.ts @@ -17,6 +17,7 @@ import { ensureAgentPoliciesFleetServerKeysAndPolicies } from './fleet_server_po jest.mock('../app_context'); jest.mock('../agent_policy'); jest.mock('../api_keys'); +jest.mock('../agent_policies/bump_agent_policies_task'); const mockedEnsureDefaultEnrollmentAPIKeyForAgentPolicy = jest.mocked( ensureDefaultEnrollmentAPIKeyForAgentPolicy diff --git a/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts b/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts index f5ed816d96e61..cd7e91fb81ac4 100644 --- a/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts +++ b/x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts @@ -13,6 +13,7 @@ import { ensureDefaultEnrollmentAPIKeyForAgentPolicy } from '../api_keys'; import { SO_SEARCH_LIMIT } from '../../constants'; import { appContextService } from '../app_context'; import { scheduleDeployAgentPoliciesTask } from '../agent_policies/deploy_agent_policies_task'; +import { scheduleBumpAgentPoliciesTask } from '../agent_policies/bump_agent_policies_task'; export async function ensureAgentPoliciesFleetServerKeysAndPolicies({ logger, @@ -37,7 +38,6 @@ export async function ensureAgentPoliciesFleetServerKeysAndPolicies({ }); const outdatedAgentPolicyIds: Array<{ id: string; spaceId?: string }> = []; - await pMap( agentPolicies, async (agentPolicy) => { @@ -55,23 +55,23 @@ export async function ensureAgentPoliciesFleetServerKeysAndPolicies({ } ); - if (!outdatedAgentPolicyIds.length) { - return; - } + await scheduleBumpAgentPoliciesTask(appContextService.getTaskManagerStart()!); - if (appContextService.getExperimentalFeatures().asyncDeployPolicies) { - return scheduleDeployAgentPoliciesTask( - appContextService.getTaskManagerStart()!, - outdatedAgentPolicyIds - ); - } else { - return agentPolicyService - .deployPolicies( - soClient, - outdatedAgentPolicyIds.map(({ id }) => id) - ) - .catch((error) => { - logger.warn(`Error deploying policies: ${error.message}`, { error }); - }); + if (outdatedAgentPolicyIds.length) { + if (appContextService.getExperimentalFeatures().asyncDeployPolicies) { + return scheduleDeployAgentPoliciesTask( + appContextService.getTaskManagerStart()!, + outdatedAgentPolicyIds + ); + } else { + return agentPolicyService + .deployPolicies( + soClient, + outdatedAgentPolicyIds.map(({ id }) => id) + ) + .catch((error) => { + logger.warn(`Error deploying policies: ${error.message}`, { error }); + }); + } } } diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts index 5bf5116128d94..48f186fed553f 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts @@ -9,12 +9,7 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { appContextService } from '../app_context'; -import { - isAgentlessApiEnabled, - isAgentlessEnabled, - isDefaultAgentlessPolicyEnabled, - prependAgentlessApiBasePathToEndpoint, -} from './agentless'; +import { isAgentlessEnabled, prependAgentlessApiBasePathToEndpoint } from './agentless'; jest.mock('../app_context'); @@ -23,7 +18,7 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({ ...securityMock.createSetup(), })); -describe('isAgentlessApiEnabled', () => { +describe('isAgentlessEnabled', () => { afterEach(() => { jest.clearAllMocks(); mockedAppContextService.getConfig.mockReset(); @@ -36,7 +31,7 @@ describe('isAgentlessApiEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: false } as any); - expect(isAgentlessApiEnabled()).toBe(false); + expect(isAgentlessEnabled()).toBe(false); }); it('should return false if cloud is enabled but agentless is not', () => { @@ -47,7 +42,7 @@ describe('isAgentlessApiEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessApiEnabled()).toBe(false); + expect(isAgentlessEnabled()).toBe(false); }); it('should return true if cloud is enabled and agentless is enabled', () => { @@ -58,109 +53,10 @@ describe('isAgentlessApiEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessApiEnabled()).toBe(true); - }); -}); - -describe('isDefaultAgentlessPolicyEnabled', () => { - afterEach(() => { - jest.clearAllMocks(); - mockedAppContextService.getConfig.mockReset(); - }); - - it('should return false if serverless is not enabled', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isServerlessEnabled: false } as any); - - expect(isDefaultAgentlessPolicyEnabled()).toBe(false); - }); - - it('should return false if serverless is enabled but agentless is not', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - - expect(isDefaultAgentlessPolicyEnabled()).toBe(false); - }); - - it('should return true if serverless is enabled and agentless is enabled', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - - expect(isDefaultAgentlessPolicyEnabled()).toBe(true); - }); -}); - -describe('isAgentlessEnabled', () => { - afterEach(() => { - jest.clearAllMocks(); - mockedAppContextService.getConfig.mockReset(); - }); - - it('should return false if cloud and serverless are not enabled', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: false } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isServerlessEnabled: false } as any); - - expect(isAgentlessEnabled()).toBe(false); - }); - - it('should return false if cloud is enabled but agentless is not', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isServerlessEnabled: false } as any); - - expect(isAgentlessEnabled()).toBe(false); - }); - - it('should return false if serverless is enabled but agentless is not', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: false } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: true } as any); - - expect(isAgentlessEnabled()).toBe(false); - }); - - it('should return true if cloud is enabled and agentless is enabled', () => { - jest - .spyOn(appContextService, 'getConfig') - .mockReturnValue({ agentless: { enabled: true } } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); - - expect(isAgentlessEnabled()).toBe(true); - }); - - it('should return true if serverless is enabled and agentless is enabled', () => { - jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: true } as any); - expect(isAgentlessEnabled()).toBe(true); }); }); + describe('prependAgentlessApiBasePathToEndpoint', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index 0f5d4e9d1de85..019c035d55e27 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -9,20 +9,11 @@ import { appContextService } from '..'; import type { FleetConfigType } from '../../config'; export { isOnlyAgentlessIntegration } from '../../../common/services/agentless_policy_helper'; -export const isAgentlessApiEnabled = () => { +export const isAgentlessEnabled = () => { const cloudSetup = appContextService.getCloud(); const isHosted = cloudSetup?.isCloudEnabled || cloudSetup?.isServerlessEnabled; return Boolean(isHosted && appContextService.getConfig()?.agentless?.enabled); }; -export const isDefaultAgentlessPolicyEnabled = () => { - const cloudSetup = appContextService.getCloud && appContextService.getCloud(); - return Boolean( - cloudSetup?.isServerlessEnabled && appContextService.getExperimentalFeatures().agentless - ); -}; -export const isAgentlessEnabled = () => { - return isAgentlessApiEnabled() || isDefaultAgentlessPolicyEnabled(); -}; const AGENTLESS_ESS_API_BASE_PATH = '/api/v1/ess'; const AGENTLESS_SERVERLESS_API_BASE_PATH = '/api/v1/serverless'; 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 c5f3e94868cf7..bbbb94b28cd4c 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -172,6 +172,16 @@ export const PackagePolicyBaseSchema = { ), ]) ), + supports_agentless: schema.maybe( + schema.nullable( + schema.boolean({ + defaultValue: false, + meta: { + description: 'Indicates whether the package policy belongs to an agentless agent policy.', + }, + }) + ) + ), }; export const NewPackagePolicySchema = schema.object({ @@ -286,6 +296,16 @@ export const SimplifiedPackagePolicyBaseSchema = schema.object({ output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), vars: schema.maybe(SimplifiedVarsSchema), inputs: SimplifiedPackagePolicyInputsSchema, + supports_agentless: schema.maybe( + schema.nullable( + schema.boolean({ + defaultValue: false, + meta: { + description: 'Indicates whether the package policy belongs to an agentless agent policy.', + }, + }) + ) + ), }); export const SimplifiedPackagePolicyPreconfiguredSchema = SimplifiedPackagePolicyBaseSchema.extends( diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index f9db02b92a659..a5fb3e9e034ed 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -527,6 +527,8 @@ export const GetAgentStatusResponseSchema = schema.object({ export const GetAgentDataRequestSchema = { query: schema.object({ agentsIds: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + pkgName: schema.maybe(schema.string()), + pkgVersion: schema.maybe(schema.string()), previewData: schema.boolean({ defaultValue: false }), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts index c40dcc0b63596..64684e51350d4 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -61,7 +61,9 @@ export const SettingsResponseSchema = schema.object({ use_space_awareness_migration_status: schema.maybe( schema.oneOf([schema.literal('pending'), schema.literal('success'), schema.literal('error')]) ), - use_space_awareness_migration_started_at: schema.maybe(schema.string()), + use_space_awareness_migration_started_at: schema.maybe( + schema.oneOf([schema.literal(null), schema.string()]) + ), delete_unenrolled_agents: schema.maybe( schema.object({ enabled: schema.boolean(), diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts index 31207dda64bc8..6f415eea9eb61 100644 --- a/x-pack/plugins/fleet/server/types/so_attributes.ts +++ b/x-pack/plugins/fleet/server/types/so_attributes.ts @@ -137,6 +137,7 @@ export interface PackagePolicySOAttributes { }; agents?: number; overrides?: any | null; + bump_agent_policy_revision?: boolean; } interface OutputSoBaseAttributes { diff --git a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx index 22790bc7f639b..28ee3bd0fd17e 100644 --- a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx @@ -10,7 +10,7 @@ import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { createMockGraphStore } from '../state_management/mocks'; import { Workspace } from '../types'; -import { renderHook, act, RenderHookOptions } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { ContentClient } from '@kbn/content-management-plugin/public'; jest.mock('react-router-dom', () => { @@ -51,15 +51,16 @@ describe('use_workspace_loader', () => { }; it('should not redirect if outcome is exactMatch', async () => { - await act(async () => { - renderHook( - () => useWorkspaceLoader(defaultProps), - defaultProps as RenderHookOptions<UseWorkspaceLoaderProps> - ); + renderHook((props) => useWorkspaceLoader(props), { + initialProps: defaultProps, + }); + + await waitFor(() => { + expect(defaultProps.spaces?.ui.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(defaultProps.store.dispatch).toHaveBeenCalled(); }); - expect(defaultProps.spaces?.ui.redirectLegacyUrl).not.toHaveBeenCalled(); - expect(defaultProps.store.dispatch).toHaveBeenCalled(); }); + it('should redirect if outcome is aliasMatch', async () => { const props = { ...defaultProps, @@ -77,16 +78,16 @@ describe('use_workspace_loader', () => { }, } as unknown as UseWorkspaceLoaderProps; - await act(async () => { - renderHook( - () => useWorkspaceLoader(props), - props as RenderHookOptions<UseWorkspaceLoaderProps> - ); - }); - expect(props.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ - path: '#/workspace/aliasTargetId?query={}', - aliasPurpose: 'savedObjectConversion', - objectNoun: 'Graph', + renderHook((_props) => useWorkspaceLoader(_props), { + initialProps: props, }); + + await waitFor(() => + expect(props.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ + path: '#/workspace/aliasTargetId?query={}', + aliasPurpose: 'savedObjectConversion', + objectNoun: 'Graph', + }) + ); }); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx index 5a97dadc870cb..4badcc04540b1 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx @@ -727,6 +727,11 @@ describe('<IndexDetailsPage />', () => { isActive: true, hasAtLeast: jest.fn((type) => true), }; + const INFERENCE_LOCATOR = 'SEARCH_INFERENCE_ENDPOINTS'; + const createMockLocator = (id: string) => ({ + useUrl: jest.fn().mockReturnValue('https://redirect.me/to/inference_endpoints'), + }); + const mockInferenceManagementLocator = createMockLocator(INFERENCE_LOCATOR); beforeEach(async () => { httpRequestsMockHelpers.setInferenceModels({ data: [ @@ -750,7 +755,9 @@ describe('<IndexDetailsPage />', () => { docLinks: { links: { ml: '', - enterpriseSearch: '', + inferenceManagement: { + inferenceAPIDocumentation: 'https://abc.com/inference-api-create', + }, }, }, core: { @@ -819,6 +826,20 @@ describe('<IndexDetailsPage />', () => { }, }, }, + share: { + url: { + locators: { + get: jest.fn((id) => { + switch (id) { + case INFERENCE_LOCATOR: + return mockInferenceManagementLocator; + default: + throw new Error(`Unknown locator id: ${id}`); + } + }), + }, + }, + }, }, }, }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx index 2fb9165e8fd10..81272dc1c4c8d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/select_inference_id.test.tsx @@ -9,7 +9,7 @@ import { Form, useForm, } from '../../../public/application/components/mappings_editor/shared_imports'; -import { registerTestBed } from '@kbn/test-jest-helpers'; +import { findTestSubject, registerTestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { SelectInferenceId, @@ -20,6 +20,11 @@ import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; const createInferenceEndpointMock = jest.fn(); const mockDispatch = jest.fn(); +const INFERENCE_LOCATOR = 'SEARCH_INFERENCE_ENDPOINTS'; +const createMockLocator = (id: string) => ({ + useUrl: jest.fn().mockReturnValue('https://redirect.me/to/inference_endpoints'), +}); +const mockInferenceManagementLocator = createMockLocator(INFERENCE_LOCATOR); jest.mock('../../../public/application/app_context', () => ({ useAppContext: jest.fn().mockReturnValue({ @@ -33,8 +38,8 @@ jest.mock('../../../public/application/app_context', () => ({ }, docLinks: { links: { - enterpriseSearch: { - inferenceApiCreate: 'https://abc.com/inference-api-create', + inferenceManagement: { + inferenceAPIDocumentation: 'https://abc.com/inference-api-create', }, }, }, @@ -47,6 +52,20 @@ jest.mock('../../../public/application/app_context', () => ({ }, }, }, + share: { + url: { + locators: { + get: jest.fn((id) => { + switch (id) { + case INFERENCE_LOCATOR: + return mockInferenceManagementLocator; + default: + throw new Error(`Unknown locator id: ${id}`); + } + }), + }, + }, + }, }, }), })); @@ -133,4 +152,34 @@ describe('SelectInferenceId', () => { expect(find('data-inference-endpoint-list').contains('endpoint-2')).toBe(true); expect(find('data-inference-endpoint-list').contains('endpoint-3')).toBe(false); }); + + it('select the first endpoint by default', () => { + find('inferenceIdButton').simulate('click'); + const defaultElser = findTestSubject( + find('data-inference-endpoint-list'), + 'custom-inference_.preconfigured-elser' + ); + expect(defaultElser.prop('aria-checked')).toEqual(true); + }); + + it('does not select the other endpoints by default', () => { + find('inferenceIdButton').simulate('click'); + const defaultE5 = findTestSubject( + find('data-inference-endpoint-list'), + 'custom-inference_.preconfigured-e5' + ); + expect(defaultE5.prop('aria-checked')).toEqual(false); + + const endpoint1 = findTestSubject( + find('data-inference-endpoint-list'), + 'custom-inference_endpoint-1' + ); + expect(endpoint1.prop('aria-checked')).toEqual(false); + + const endpoint2 = findTestSubject( + find('data-inference-endpoint-list'), + 'custom-inference_endpoint-2' + ); + expect(endpoint2.prop('aria-checked')).toEqual(false); + }); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 7ba6832b8833d..4992c1391635f 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -315,7 +315,7 @@ describe('<TemplateCreate />', () => { expect(exists('indexModeCallout')).toBe(true); expect(find('indexModeCallout').text()).toContain( - 'The index.mode setting has been set to Standard within template Logistics.' + 'The index.mode setting has been set to Standard within the Logistics step.' ); }); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 1a8276d76b7fc..6df01a109a036 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -79,6 +79,7 @@ export interface AppDependencies { docLinks: DocLinksStart; kibanaVersion: SemVer; overlays: OverlayStart; + canUseSyntheticSource: boolean; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_datastreams_rollover/use_datastreams_rollover.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_datastreams_rollover/use_datastreams_rollover.test.tsx index 903a9187a7335..d4ac28521b31e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_datastreams_rollover/use_datastreams_rollover.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_datastreams_rollover/use_datastreams_rollover.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useComponentTemplatesContext } from '../../component_templates_context'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/use_step_from_query_string.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/use_step_from_query_string.test.tsx index cb26f40c91ae3..6e04cdc37e1fe 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/use_step_from_query_string.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/use_step_from_query_string.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { useStepFromQueryString } from './use_step_from_query_string'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx index 4c73fd1037dda..5bc28052b73e2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx @@ -28,6 +28,14 @@ const setup = (props: any = { onUpdate() {} }, appDependencies?: any) => { return testBed; }; +const getContext = (sourceFieldEnabled: boolean = true, canUseSyntheticSource: boolean = true) => + ({ + config: { + enableMappingsSourceFieldSection: sourceFieldEnabled, + }, + canUseSyntheticSource, + } as unknown as AppDependencies); + describe('Mappings editor: configuration form', () => { let testBed: TestBed<TestSubjects>; @@ -49,14 +57,8 @@ describe('Mappings editor: configuration form', () => { describe('_source field', () => { it('renders the _source field when it is enabled', async () => { - const ctx = { - config: { - enableMappingsSourceFieldSection: true, - }, - } as unknown as AppDependencies; - await act(async () => { - testBed = setup({ esNodesPlugins: [] }, ctx); + testBed = setup({ esNodesPlugins: [] }, getContext()); }); testBed.component.update(); const { exists } = testBed; @@ -65,19 +67,37 @@ describe('Mappings editor: configuration form', () => { }); it("doesn't render the _source field when it is disabled", async () => { - const ctx = { - config: { - enableMappingsSourceFieldSection: false, - }, - } as unknown as AppDependencies; - await act(async () => { - testBed = setup({ esNodesPlugins: [] }, ctx); + testBed = setup({ esNodesPlugins: [] }, getContext(false)); }); testBed.component.update(); const { exists } = testBed; expect(exists('sourceField')).toBe(false); }); + + it('has synthetic option if `canUseSyntheticSource` is set to true', async () => { + await act(async () => { + testBed = setup({ esNodesPlugins: [] }, getContext(true, true)); + }); + testBed.component.update(); + const { exists, find } = testBed; + + // Clicking on the field to open the options dropdown + find('sourceValueField').simulate('click'); + expect(exists('syntheticSourceFieldOption')).toBe(true); + }); + + it("doesn't have synthetic option if `canUseSyntheticSource` is set to false", async () => { + await act(async () => { + testBed = setup({ esNodesPlugins: [] }, getContext(true, false)); + }); + testBed.component.update(); + const { exists, find } = testBed; + + // Clicking on the field to open the options dropdown + find('sourceValueField').simulate('click'); + expect(exists('syntheticSourceFieldOption')).toBe(false); + }); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index ee3b3e72e7c19..cb8a1efbf9c70 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -28,22 +28,9 @@ describe('Mappings editor: core', () => { let onChangeHandler: jest.Mock = jest.fn(); let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); let testBed: MappingsEditorTestBed; - let hasEnterpriseLicense = true; - const mockLicenseCheck = jest.fn((type: any) => hasEnterpriseLicense); const appDependencies = { plugins: { ml: { mlApi: {} }, - licensing: { - license$: { - subscribe: jest.fn((callback: any) => { - callback({ - isActive: true, - hasAtLeast: mockLicenseCheck, - }); - return { unsubscribe: jest.fn() }; - }), - }, - }, }, }; @@ -314,6 +301,7 @@ describe('Mappings editor: core', () => { config: { enableMappingsSourceFieldSection: true, }, + canUseSyntheticSource: true, ...appDependencies, }; @@ -512,8 +500,7 @@ describe('Mappings editor: core', () => { }); ['logsdb', 'time_series'].forEach((indexMode) => { - it(`defaults to 'synthetic' with ${indexMode} index mode prop on enterprise license`, async () => { - hasEnterpriseLicense = true; + it(`defaults to 'synthetic' with ${indexMode} index mode prop when 'canUseSyntheticSource' is set to true`, async () => { await act(async () => { testBed = setup( { @@ -537,8 +524,7 @@ describe('Mappings editor: core', () => { expect(find('sourceValueField').prop('value')).toBe('synthetic'); }); - it(`defaults to 'standard' with ${indexMode} index mode prop on basic license`, async () => { - hasEnterpriseLicense = false; + it(`defaults to 'standard' with ${indexMode} index mode prop when 'canUseSyntheticSource' is set to true`, async () => { await act(async () => { testBed = setup( { @@ -546,7 +532,7 @@ describe('Mappings editor: core', () => { onChange: onChangeHandler, indexMode, }, - ctx + { ...ctx, canUseSyntheticSource: false } ); }); testBed.component.update(); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 00ce2d02a1baa..6ddaf8e48fe42 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -39,6 +39,31 @@ interface SerializedSourceField { excludes?: string[]; } +const serializeSourceField = (sourceField: any): SerializedSourceField | undefined => { + if (sourceField?.option === SYNTHETIC_SOURCE_OPTION) { + return { mode: SYNTHETIC_SOURCE_OPTION }; + } + if (sourceField?.option === DISABLED_SOURCE_OPTION) { + return { enabled: false }; + } + if (sourceField?.option === STORED_SOURCE_OPTION) { + return { + mode: 'stored', + includes: sourceField.includes, + excludes: sourceField.excludes, + }; + } + if (sourceField?.includes || sourceField?.excludes) { + // If sourceField?.option is undefined, the user hasn't explicitly selected + // this option, so don't include the `mode` property + return { + includes: sourceField.includes, + excludes: sourceField.excludes, + }; + } + return undefined; +}; + export const formSerializer = (formData: GenericObject) => { const { dynamicMapping, sourceField, metaField, _routing, _size, subobjects } = formData; @@ -48,30 +73,12 @@ export const formSerializer = (formData: GenericObject) => { ? 'strict' : dynamicMapping?.enabled; - const _source = - sourceField?.option === SYNTHETIC_SOURCE_OPTION - ? { mode: SYNTHETIC_SOURCE_OPTION } - : sourceField?.option === DISABLED_SOURCE_OPTION - ? { enabled: false } - : sourceField?.option === STORED_SOURCE_OPTION - ? { - mode: 'stored', - includes: sourceField?.includes, - excludes: sourceField?.excludes, - } - : sourceField?.includes || sourceField?.excludes - ? { - includes: sourceField?.includes, - excludes: sourceField?.excludes, - } - : undefined; - const serialized = { dynamic, numeric_detection: dynamicMapping?.numeric_detection, date_detection: dynamicMapping?.date_detection, dynamic_date_formats: dynamicMapping?.dynamic_date_formats, - _source: _source as SerializedSourceField, + _source: serializeSourceField(sourceField), _meta: metaField, _routing, _size, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index 2e8f9fb88f87d..c1709fa135035 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut, EuiText } from '@elastic/eui'; -import { useMappingsState } from '../../../mappings_state_context'; +import { useAppContext } from '../../../../../app_context'; import { documentationService } from '../../../../../services/documentation'; import { UseField, FormDataProvider, FormRow, SuperSelectField } from '../../../shared_imports'; import { ComboBoxOption } from '../../../types'; @@ -24,7 +24,7 @@ import { } from './constants'; export const SourceFieldSection = () => { - const state = useMappingsState(); + const { canUseSyntheticSource } = useAppContext(); const renderOptionDropdownDisplay = (option: SourceOptionKey) => ( <Fragment> @@ -44,7 +44,7 @@ export const SourceFieldSection = () => { }, ]; - if (state.hasEnterpriseLicense) { + if (canUseSyntheticSource) { sourceValueOptions.push({ value: SYNTHETIC_SOURCE_OPTION, inputDisplay: sourceOptionLabels[SYNTHETIC_SOURCE_OPTION], diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx index a9c54aee80360..813cc1023c06d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx @@ -80,13 +80,15 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({ value, }) => { const { - core: { application, http }, + core: { application }, docLinks, - plugins: { ml }, + plugins: { ml, share }, } = useAppContext(); const config = getFieldConfig('inference_id'); - const inferenceEndpointsPageLink = `${http.basePath.get()}/app/enterprise_search/relevance/inference_endpoints`; + const inferenceEndpointsPageLink = share?.url.locators + .get('SEARCH_INFERENCE_ENDPOINTS') + ?.useUrl({}); const [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] = useState<boolean>(false); const [availableTrainedModels, setAvailableTrainedModels] = useState< @@ -130,6 +132,14 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({ 'data-test-subj': `custom-inference_${endpoint.inference_id}`, checked: value === endpoint.inference_id ? 'on' : undefined, })); + /** + * Adding this check to ensure we have the preconfigured elser endpoint selected by default. + */ + const hasInferenceSelected = newOptions.some((option) => option.checked === 'on'); + if (!hasInferenceSelected && newOptions.length > 0) { + newOptions[0].checked = 'on'; + } + if (value && !newOptions.find((option) => option.label === value)) { // Sometimes we create a new endpoint but the backend is slow in updating so we need to optimistically update const newOption: EuiSelectableOption = { @@ -224,24 +234,28 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({ panelPaddingSize="m" closePopover={() => setIsInferencePopoverVisible(!isInferencePopoverVisible)} > - <EuiContextMenuPanel> - <EuiContextMenuItem - key="manageInferenceEndpointButton" - icon="gear" - size="s" - data-test-subj="manageInferenceEndpointButton" - onClick={async () => { - application.navigateToUrl(inferenceEndpointsPageLink); - }} - > - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.manageInferenceEndpointButton', - { - defaultMessage: 'Manage Inference Endpoints', - } - )} - </EuiContextMenuItem> - </EuiContextMenuPanel> + {inferenceEndpointsPageLink && ( + <EuiContextMenuPanel> + <EuiContextMenuItem + key="manageInferenceEndpointButton" + icon="gear" + size="s" + data-test-subj="manageInferenceEndpointButton" + href={inferenceEndpointsPageLink} + onClick={(e) => { + e.preventDefault(); + application.navigateToUrl(inferenceEndpointsPageLink); + }} + > + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.manageInferenceEndpointButton', + { + defaultMessage: 'Manage Inference Endpoints', + } + )} + </EuiContextMenuItem> + </EuiContextMenuPanel> + )} <EuiHorizontalRule margin="none" /> <EuiPanel color="transparent" paddingSize="s"> <EuiTitle size="xxxs"> @@ -267,6 +281,7 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({ searchable isLoading={isLoading} singleSelection="always" + defaultChecked searchProps={{ compressed: true, placeholder: i18n.translate( @@ -292,7 +307,7 @@ const SelectInferenceIdContent: React.FC<SelectInferenceIdContentProps> = ({ <EuiHorizontalRule margin="none" /> <EuiContextMenuItem icon={<EuiIcon type="help" color="primary" />} size="s"> <EuiLink - href={docLinks.links.enterpriseSearch.inferenceApiCreate} + href={docLinks.links.inferenceManagement.inferenceAPIDocumentation} target="_blank" data-test-subj="learn-how-to-create-inference-endpoints" > diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts index 65415a287d94c..e0ce2db2446ee 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { CustomInferenceEndpointConfig, SemanticTextField } from '../../../../../types'; import { useSemanticText } from './use_semantic_text'; import { act } from 'react-dom/test-utils'; @@ -13,15 +13,15 @@ import { act } from 'react-dom/test-utils'; jest.mock('../../../../../../../../hooks/use_details_page_mappings_model_management', () => ({ useDetailsPageMappingsModelManagement: () => ({ fetchInferenceToModelIdMap: () => ({ - e5: { + '.preconfigured_elser': { isDeployed: false, isDeployable: true, - trainedModelId: '.multilingual-e5-small', + trainedModelId: '.elser_model_2', }, - elser_model_2: { + '.preconfigured_e5': { isDeployed: false, isDeployable: true, - trainedModelId: '.elser_model_2', + trainedModelId: '.multilingual-e5-small', }, openai: { isDeployed: false, @@ -49,13 +49,13 @@ const mockField: Record<string, SemanticTextField> = { elser_model_2: { name: 'name', type: 'semantic_text', - inference_id: 'elser_model_2', + inference_id: '.preconfigured_elser', reference_field: 'title', }, e5: { name: 'name', type: 'semantic_text', - inference_id: 'e5', + inference_id: '.preconfigured_e5', reference_field: 'title', }, openai: { @@ -100,15 +100,15 @@ const mockDispatch = jest.fn(); jest.mock('../../../../../mappings_state_context', () => ({ useMappingsState: jest.fn().mockReturnValue({ inferenceToModelIdMap: { - e5: { + '.preconfigured_elser': { isDeployed: false, isDeployable: true, - trainedModelId: '.multilingual-e5-small', + trainedModelId: '.elser_model_2', }, - elser_model_2: { + '.preconfigured_e5': { isDeployed: false, isDeployable: true, - trainedModelId: '.elser_model_2', + trainedModelId: '.multilingual-e5-small', }, openai: { isDeployed: false, @@ -142,7 +142,7 @@ jest.mock('../../../../../../../services/api', () => ({ getInferenceEndpoints: jest.fn().mockResolvedValue({ data: [ { - inference_id: 'e5', + inference_id: '.preconfigured_e5', task_type: 'text_embedding', service: 'elasticsearch', service_settings: { @@ -212,28 +212,6 @@ describe('useSemanticText', () => { mockConfig.openai.modelConfig ); }); - it('should handle semantic text with inference endpoint created from flyout correctly', async () => { - const { result } = renderHook(() => - useSemanticText({ - form: mockForm.elasticModelEndpointCreatedfromFlyout, - setErrorsInTrainedModelDeployment: jest.fn(), - ml: mlMock, - }) - ); - await act(async () => { - result.current.handleSemanticText(mockField.my_elser_endpoint, mockConfig.elser); - }); - - expect(mockDispatch).toHaveBeenCalledWith({ - type: 'field.add', - value: mockField.my_elser_endpoint, - }); - expect(mlMock.mlApi.inferenceModels.createInferenceEndpoint).toHaveBeenCalledWith( - 'my_elser_endpoint', - 'sparse_embedding', - mockConfig.elser.modelConfig - ); - }); it('should handle semantic text correctly', async () => { const { result } = renderHook(() => @@ -252,20 +230,6 @@ describe('useSemanticText', () => { type: 'field.add', value: mockField.elser_model_2, }); - expect(mlMock.mlApi.inferenceModels.createInferenceEndpoint).toHaveBeenCalledWith( - 'elser_model_2', - 'sparse_embedding', - { - service: 'elser', - service_settings: { - adaptive_allocations: { - enabled: true, - }, - num_threads: 1, - model_id: '.elser_model_2', - }, - } - ); }); it('does not call create inference endpoint api, if default endpoint already exists', async () => { const { result } = renderHook(() => diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts index a464a279a8ddf..a7b380fd120cd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts @@ -19,7 +19,6 @@ import { useMLModelNotificationToasts } from '../../../../../../../../hooks/use_ import { getInferenceEndpoints } from '../../../../../../../services/api'; import { getFieldByPathName } from '../../../../../lib/utils'; -import { ELSER_PRECONFIGURED_ENDPOINTS } from '../../../../../constants'; interface UseSemanticTextProps { form: FormHook<Field, Field>; @@ -63,9 +62,6 @@ export function useSemanticText(props: UseSemanticTextProps) { if (!form.getFormData().reference_field) { form.setFieldValue('reference_field', referenceField); } - if (!form.getFormData().inference_id) { - form.setFieldValue('inference_id', ELSER_PRECONFIGURED_ENDPOINTS); - } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [fieldTypeValue]); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index a67a7df0acb7b..33c51a3cb644b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; import { NormalizedField, NormalizedFields, State } from '../../../types'; import { getTypeLabelFromField } from '../../../lib'; -import { - CHILD_FIELD_INDENT_SIZE, - ELSER_PRECONFIGURED_ENDPOINTS, - LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, -} from '../../../constants'; +import { CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER } from '../../../constants'; import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; @@ -109,7 +105,6 @@ function FieldListItemComponent( const indent = treeDepth * CHILD_FIELD_INDENT_SIZE - substractIndentAmount; const isSemanticText = source.type === 'semantic_text'; - const inferenceId: string = (source.inference_id as string) ?? ELSER_PRECONFIGURED_ENDPOINTS; const indentCreateField = (treeDepth + 1) * CHILD_FIELD_INDENT_SIZE + @@ -298,7 +293,7 @@ function FieldListItemComponent( {isSemanticText && ( <EuiFlexItem grow={false}> - <EuiBadge color="hollow">{inferenceId}</EuiBadge> + <EuiBadge color="hollow">{source.inference_id as string}</EuiBadge> </EuiFlexItem> )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts index f8c6da8f7cddb..b839caf75b242 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts @@ -13,9 +13,3 @@ export const INDEX_DEFAULT = 'index_default'; export const STANDARD = 'standard'; - -/* - This will be repalce once we add default elser inference_id - with the index mapping response. -*/ -export const ELSER_PRECONFIGURED_ENDPOINTS = '.elser-2-elasticsearch'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index 7e10c5d5deaa7..749150cf2d671 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -1126,22 +1126,10 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, inference_id: { fieldConfig: { - defaultValue: 'elser_model_2', + defaultValue: '', label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.inferenceIdLabel', { defaultMessage: 'Select an inference endpoint:', }), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.idxMgmt.mappingsEditor.parameters.validations.inferenceIdIsRequiredErrorMessage', - { - defaultMessage: 'Inference ID is required.', - } - ) - ), - }, - ], }, schema: t.string, }, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 872c62bc6f7a7..58b40293f64f2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -421,7 +421,6 @@ describe('utils', () => { selectedDataTypes: ['Boolean'], }, inferenceToModelIdMap: {}, - hasEnterpriseLicense: true, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; test('returns list of matching fields with search term', () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index cc87c3cd614e3..9f59f4959bed5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -9,7 +9,6 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; -import { ILicense } from '@kbn/licensing-plugin/common/types'; import { useAppContext } from '../../app_context'; import { IndexMode } from '../../../../common/types/data_streams'; import { @@ -61,9 +60,7 @@ export interface Props { export const MappingsEditor = React.memo( ({ onChange, value, docLinks, indexSettings, esNodesPlugins, indexMode }: Props) => { - const { - plugins: { licensing }, - } = useAppContext(); + const { canUseSyntheticSource } = useAppContext(); const { parsedDefaultValue, multipleMappingsDeclared } = useMemo<MappingsEditorParsedMetadata>( () => parseMappings(value), [value] @@ -128,39 +125,22 @@ export const MappingsEditor = React.memo( [dispatch] ); - const [isLicenseCheckComplete, setIsLicenseCheckComplete] = useState(false); - useEffect(() => { - const subscription = licensing?.license$.subscribe((license: ILicense) => { - dispatch({ - type: 'hasEnterpriseLicense.update', - value: license.isActive && license.hasAtLeast('enterprise'), - }); - setIsLicenseCheckComplete(true); - }); - - return () => subscription?.unsubscribe(); - }, [dispatch, licensing]); - useEffect(() => { if ( - isLicenseCheckComplete && !state.configuration.defaultValue._source && (indexMode === LOGSDB_INDEX_MODE || indexMode === TIME_SERIES_MODE) ) { - if (state.hasEnterpriseLicense) { + if (canUseSyntheticSource) { + // If the source field is undefined (hasn't been set in the form) + // and if the user has selected a `logsdb` or `time_series` index mode in the Logistics step, + // update the form data with synthetic _source dispatch({ type: 'configuration.save', value: { ...state.configuration.defaultValue, _source: { mode: 'synthetic' } } as any, }); } } - }, [ - indexMode, - dispatch, - state.configuration, - state.hasEnterpriseLicense, - isLicenseCheckComplete, - ]); + }, [indexMode, dispatch, state.configuration, canUseSyntheticSource]); const tabToContentMap = { fields: ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx index 4fd89ad0e25ad..ac19c5395f974 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -60,7 +60,6 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr selectedDataTypes: [], }, inferenceToModelIdMap: {}, - hasEnterpriseLicense: false, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index ecb9648c34d00..626ee0e839a8a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -629,11 +629,5 @@ export const reducer = (state: State, action: Action): State => { inferenceToModelIdMap: action.value.inferenceToModelIdMap, }; } - case 'hasEnterpriseLicense.update': { - return { - ...state, - hasEnterpriseLicense: action.value, - }; - } } }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts index 43b3a7dde3b16..f40fe420eb3be 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -108,7 +108,6 @@ export interface State { }; templates: TemplatesFormState; inferenceToModelIdMap?: InferenceToModelIdMap; - hasEnterpriseLicense: boolean; mappingViewFields: NormalizedFields; // state of the incoming index mappings, separate from the editor state above } @@ -141,7 +140,6 @@ export type Action = | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } | { type: 'search:update'; value: string } | { type: 'validity:update'; value: boolean } - | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } } - | { type: 'hasEnterpriseLicense.update'; value: boolean }; + | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } }; export type Dispatch = (action: Action) => void; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx index 38a11f03c7ee6..cc12cfc988d5b 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx @@ -92,7 +92,7 @@ export const StepSettings: React.FunctionComponent<Props> = React.memo( title={ <FormattedMessage id="xpack.idxMgmt.formWizard.stepSettings.indexModeCallout.title" - defaultMessage="The {settingName} setting has been set to {indexMode} within template {logisticsLink}. Any changes to {settingName} set on this page will be overwritten by the Logistics selection." + defaultMessage="The {settingName} setting has been set to {indexMode} within the {logisticsLink}. Any changes to {settingName} set on this page will be overwritten by the Logistics selection." values={{ settingName: ( <EuiCode> @@ -110,7 +110,7 @@ export const StepSettings: React.FunctionComponent<Props> = React.memo( {i18n.translate( 'xpack.idxMgmt.formWizard.stepSettings.indexModeCallout.logisticsLinkLabel', { - defaultMessage: 'Logistics', + defaultMessage: 'Logistics step', } )} </EuiLink> diff --git a/x-pack/plugins/index_management/public/application/hooks/redirect_path.test.tsx b/x-pack/plugins/index_management/public/application/hooks/redirect_path.test.tsx index 5b189ccbc253c..83bd06a8dd491 100644 --- a/x-pack/plugins/index_management/public/application/hooks/redirect_path.test.tsx +++ b/x-pack/plugins/index_management/public/application/hooks/redirect_path.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { coreMock } from '@kbn/core/public/mocks'; import { createMemoryHistory } from 'history'; import { useRedirectPath } from './redirect_path'; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index da17bc4706c02..48a45579a01fc 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -57,6 +57,7 @@ export function getIndexManagementDependencies({ cloud, startDependencies, uiMetricService, + canUseSyntheticSource, }: { core: CoreStart; usageCollection: UsageCollectionSetup; @@ -68,6 +69,7 @@ export function getIndexManagementDependencies({ cloud?: CloudSetup; startDependencies: StartDependencies; uiMetricService: UiMetricService; + canUseSyntheticSource: boolean; }): AppDependencies { const { docLinks, application, uiSettings, settings } = core; const { url } = startDependencies.share; @@ -100,6 +102,7 @@ export function getIndexManagementDependencies({ docLinks, kibanaVersion, overlays: core.overlays, + canUseSyntheticSource, }; } @@ -112,6 +115,7 @@ export async function mountManagementSection({ kibanaVersion, config, cloud, + canUseSyntheticSource, }: { coreSetup: CoreSetup<StartDependencies>; usageCollection: UsageCollectionSetup; @@ -121,6 +125,7 @@ export async function mountManagementSection({ kibanaVersion: SemVer; config: AppDependencies['config']; cloud?: CloudSetup; + canUseSyntheticSource: boolean; }) { const { element, setBreadcrumbs, history } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -148,6 +153,7 @@ export async function mountManagementSection({ startDependencies, uiMetricService, usageCollection, + canUseSyntheticSource, }); const unmountAppCallback = renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/public/hooks/use_details_page_mappings_model_management.test.ts b/x-pack/plugins/index_management/public/hooks/use_details_page_mappings_model_management.test.ts index 62746791510c0..4de34b584aec5 100644 --- a/x-pack/plugins/index_management/public/hooks/use_details_page_mappings_model_management.test.ts +++ b/x-pack/plugins/index_management/public/hooks/use_details_page_mappings_model_management.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { NormalizedFields } from '../application/components/mappings_editor/types'; import { useDetailsPageMappingsModelManagement } from './use_details_page_mappings_model_management'; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 5d857d8d8ac9c..82ba6505696aa 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -20,6 +20,7 @@ import { IndexManagementPluginStart, } from '@kbn/index-management-shared-types'; import { IndexManagementLocator } from '@kbn/index-management-shared-types'; +import { Subscription } from 'rxjs'; import { setExtensionsService } from './application/store/selectors/extension_service'; import { ExtensionsService } from './services/extensions_service'; @@ -57,6 +58,8 @@ export class IndexMgmtUIPlugin enableProjectLevelRetentionChecks: boolean; enableSemanticText: boolean; }; + private canUseSyntheticSource: boolean = false; + private licensingSubscription?: Subscription; constructor(ctx: PluginInitializerContext) { // Temporary hack to provide the service instances in module files in order to avoid a big refactor @@ -113,6 +116,7 @@ export class IndexMgmtUIPlugin kibanaVersion: this.kibanaVersion, config: this.config, cloud, + canUseSyntheticSource: this.canUseSyntheticSource, }); }, }); @@ -133,6 +137,11 @@ export class IndexMgmtUIPlugin public start(coreStart: CoreStart, plugins: StartDependencies): IndexManagementPluginStart { const { fleet, usageCollection, cloud, share, console, ml, licensing } = plugins; + + this.licensingSubscription = licensing?.license$.subscribe((next) => { + this.canUseSyntheticSource = next.hasAtLeast('enterprise'); + }); + return { extensionsService: this.extensionsService.setup(), getIndexMappingComponent: (deps: { history: ScopedHistory<unknown> }) => { @@ -213,5 +222,7 @@ export class IndexMgmtUIPlugin }, }; } - public stop() {} + public stop() { + this.licensingSubscription?.unsubscribe(); + } } diff --git a/x-pack/plugins/ingest_pipelines/public/application/hooks/redirect_path.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/hooks/redirect_path.test.tsx index a387661fc1b53..25729f5ba255a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/hooks/redirect_path.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/hooks/redirect_path.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { useRedirectPath } from './redirect_path'; import { useKibana } from '../../shared_imports'; diff --git a/x-pack/plugins/integration_assistant/__jest__/fixtures/ecs_mapping.ts b/x-pack/plugins/integration_assistant/__jest__/fixtures/ecs_mapping.ts index 67e6c7d8f6a5e..8b905ccd64f96 100644 --- a/x-pack/plugins/integration_assistant/__jest__/fixtures/ecs_mapping.ts +++ b/x-pack/plugins/integration_assistant/__jest__/fixtures/ecs_mapping.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EcsMappingState } from '../../server/types'; import { SamplesFormatName } from '../../common'; export const ecsMappingExpectedResults = { @@ -480,3 +481,383 @@ export const ecsTestState = { combinedSamples: '{"test1": "test1"}', additionalProcessors: [], }; + +export const ecsPipelineState: EcsMappingState = { + lastExecutedChain: 'validateMappings', + rawSamples: [], + additionalProcessors: [], + prefixedSamples: [ + '{"xdfsfs":{"ds":{"ei":0,"event":"cert.create","uid":"efd326fc-dd13-4df8-erre-3102c2d717d3","code":"TC000I","time":"2024-02-24T06:56:50.648137154Z","cluster_name":"teleport.ericbeahan.com","cert_type":"user","identity":{"user":"teleport-admin","roles":["access","editor"],"logins":["root","ubuntu","ec2-user","-teleport-internal-join"],"expires":"2024-02-24T06:56:50.648137154Z","route_to_cluster":"teleport.ericbeahan.com","traits":{"aws_role_arns":null,"azure_identities":null,"db_names":null,"db_roles":null,"db_users":null,"gcp_service_accounts":null,"host_user_gid":[""],"host_user_uid":[""],"kubernetes_groups":null,"kubernetes_users":null,"logins":["root","ubuntu","ec2-user"],"windows_logins":null},"teleport_cluster":"teleport.ericbeahan.com","client_ip":"1.2.3.4","prev_identity_expires":"0001-01-01T00:00:00Z","private_key_policy":"none"}}}}', + '{"xdfsfs":{"ds":{"ei":0,"event":"session.start","uid":"fff30583-13be-49e8-b159-32952c6ea34f","code":"T2000I","time":"2024-02-23T18:56:57.648137154Z","cluster_name":"teleport.ericbeahan.com","user":"teleport-admin","login":"ec2-user","user_kind":1,"sid":"293fda2d-2266-4d4d-b9d1-bd5ea9dd9fc3","private_key_policy":"none","namespace":"default","server_id":"face0091-2bf1-54er-a16a-f1514b4119f4","server_hostname":"ip-172-31-8-163.us-east-2.compute.internal","server_labels":{"hostname":"ip-172-31-8-163.us-east-2.compute.internal","teleport.internal/resource-id":"dccb2999-9fb8-4169-aded-ec7a1c0a26de"},"addr.remote":"1.2.3.4:50339","proto":"ssh","size":"80:25","initial_command":[""],"session_recording":"node"}}}', + ], + combinedSamples: + '{\n "xdfsfs": {\n "ds": {\n "identity": {\n "client_ip": "1.2.3.4",\n "prev_identity_expires": "0001-01-01T00:00:00Z",\n "private_key_policy": "none"\n },\n "user": "teleport-admin",\n "login": "ec2-user",\n "user_kind": 1,\n "sid": "293fda2d-2266-4d4d-b9d1-bd5ea9dd9fc3",\n "private_key_policy": "none",\n "namespace": "default",\n "server_id": "face0091-2bf1-43fd-a16a-f1514b4119f4",\n "server_hostname": "ip-172-31-8-163.us-east-2.compute.internal",\n "server_labels": {\n "hostname": "ip-172-31-8-163.us-east-2.compute.internal",\n "teleport.internal/resource-id": "dccb2999-9fb8-4169-aded-ec7a1c0a26de"\n },\n "addr.remote": "1.2.3.4:50339",\n "proto": "ssh",\n "size": "80:25",\n "initial_command": [\n ""\n ],\n "session_recording": "node"\n }\n }\n}', + sampleChunks: [], + exAnswer: + '{\n "crowdstrike": {\n "falcon": {\n "metadata": {\n "customerIDString": null,\n "offset": null,\n "eventType": {\n "target": "event.code",\n "confidence": 0.94,\n "type": "string",\n "date_formats": []\n },\n "eventCreationTime": {\n "target": "event.created",\n "confidence": 0.85,\n "type": "date",\n "date_formats": [\n "UNIX"\n ]\n },\n "version": null,\n "event": {\n "DeviceId": null,\n "CustomerId": null,\n "Ipv": {\n "target": "network.type",\n "confidence": 0.99,\n "type": "string",\n "date_formats": []\n }\n }\n }\n }\n }\n}', + packageName: 'xdfsfs', + dataStreamName: 'ds', + finalized: false, + currentMapping: { + xdfsfs: { + ds: { + identity: { + client_ip: { + target: 'client.ip', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + prev_identity_expires: { + target: 'event.end', + confidence: 0.7, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss'Z'"], + }, + private_key_policy: null, + }, + user: { + target: 'user.name', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + login: { + target: 'user.id', + confidence: 0.8, + type: 'string', + date_formats: [], + }, + user_kind: null, + sid: { + target: 'event.id', + confidence: 0.85, + type: 'string', + date_formats: [], + }, + private_key_policy: null, + namespace: null, + server_id: { + target: 'host.id', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + server_hostname: { + target: 'host.hostname', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + server_labels: { + hostname: null, + 'teleport.internal/resource-id': null, + }, + 'addr.remote': { + target: 'source.address', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + proto: { + target: 'network.protocol', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + size: null, + initial_command: null, + session_recording: null, + }, + }, + }, + chunkMapping: { + xdfsfs: { + ds: { + ei: null, + event: { + target: 'event.action', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + uid: { + target: 'event.id', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + code: { + target: 'event.code', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + time: { + target: 'event.created', + confidence: 0.95, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"], + }, + cluster_name: { + target: 'cloud.account.name', + confidence: 0.8, + type: 'string', + date_formats: [], + }, + cert_type: null, + identity: { + user: { + target: 'user.name', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + roles: { + target: 'user.roles', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + logins: null, + expires: { + target: 'user.changes.name', + confidence: 0.7, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"], + }, + route_to_cluster: null, + traits: { + aws_role_arns: null, + azure_identities: null, + db_names: null, + db_roles: null, + db_users: null, + gcp_service_accounts: null, + host_user_gid: null, + host_user_uid: null, + kubernetes_groups: null, + kubernetes_users: null, + logins: null, + windows_logins: null, + }, + teleport_cluster: null, + client_ip: { + target: 'client.ip', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + prev_identity_expires: { + target: 'event.end', + confidence: 0.7, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss'Z'"], + }, + private_key_policy: null, + }, + user: { + target: 'user.name', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + login: { + target: 'user.id', + confidence: 0.8, + type: 'string', + date_formats: [], + }, + user_kind: null, + sid: { + target: 'event.id', + confidence: 0.85, + type: 'string', + date_formats: [], + }, + private_key_policy: null, + namespace: null, + server_id: { + target: 'host.id', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + server_hostname: { + target: 'host.hostname', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + server_labels: { + hostname: null, + 'teleport.internal/resource-id': null, + }, + 'addr.remote': { + target: 'source.address', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + proto: { + target: 'network.protocol', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + size: null, + initial_command: null, + session_recording: null, + }, + }, + }, + finalMapping: { + xdfsfs: { + ds: { + ei: null, + event: { + target: 'event.action', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + uid: { + target: 'event.id', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + code: { + target: 'event.code', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + '@timestamp': { + target: '@timestamp', + confidence: 0.95, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"], + }, + cluster_name: { + target: 'cloud.account.name', + confidence: 0.8, + type: 'string', + date_formats: [], + }, + cert_type: null, + identity: { + user: { + target: 'user.name', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + roles: { + target: 'user.roles', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + logins: null, + expires: { + target: 'user.changes.name', + confidence: 0.7, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"], + }, + route_to_cluster: null, + traits: { + aws_role_arns: null, + azure_identities: null, + db_names: null, + db_roles: null, + db_users: null, + gcp_service_accounts: null, + host_user_gid: null, + host_user_uid: null, + kubernetes_groups: null, + kubernetes_users: null, + logins: null, + windows_logins: null, + }, + teleport_cluster: null, + client_ip: { + target: 'client.ip', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + prev_identity_expires: { + target: 'event.end', + confidence: 0.7, + type: 'date', + date_formats: ["yyyy-MM-dd'T'HH:mm:ss'Z'"], + }, + private_key_policy: null, + }, + user: { + target: 'user.name', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + login: { + target: 'user.id', + confidence: 0.8, + type: 'string', + date_formats: [], + }, + user_kind: null, + sid: null, + private_key_policy: null, + namespace: null, + server_id: { + target: 'host.id', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + server_hostname: { + target: 'host.hostname', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + server_labels: { + hostname: null, + 'teleport.internal/resource-id': null, + }, + 'addr.remote': { + target: 'source.address', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + proto: { + target: 'network.protocol', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + size: null, + initial_command: null, + session_recording: null, + }, + }, + }, + useFinalMapping: true, + hasTriedOnce: true, + currentPipeline: {}, + duplicateFields: [], + missingKeys: [], + invalidEcsFields: [], + results: {}, + samplesFormat: { + name: 'json', + json_path: [], + }, + ecsVersion: '8.11.0', + ecs: '', + chunkSize: 0, +}; diff --git a/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.test.ts b/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.test.ts new file mode 100644 index 0000000000000..3dda9208c3094 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.test.ts @@ -0,0 +1,306 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ecsPipelineState } from '../../../__jest__/fixtures/ecs_mapping'; +import type { EcsMappingState } from '../../types'; +import { createPipeline } from './pipeline'; + +const state: EcsMappingState = ecsPipelineState; + +describe('Testing pipeline templates', () => { + it('handle pipeline creation', async () => { + const pipeline = createPipeline(state); + expect(pipeline.processors).toEqual([ + { + set: { field: 'ecs.version', tag: 'set_ecs_version', value: '8.11.0' }, + }, + { + set: { + field: 'originalMessage', + copy_from: 'message', + tag: 'copy_original_message', + }, + }, + { + rename: { + field: 'originalMessage', + target_field: 'event.original', + tag: 'rename_message', + ignore_missing: true, + if: 'ctx.event?.original == null', + }, + }, + { + remove: { + field: 'originalMessage', + ignore_missing: true, + tag: 'remove_copied_message', + if: 'ctx.event?.original != null', + }, + }, + { + remove: { field: 'message', ignore_missing: true, tag: 'remove_message' }, + }, + { + json: { + field: 'event.original', + tag: 'json_original', + target_field: 'xdfsfs.ds', + }, + }, + { + rename: { + field: 'xdfsfs.ds.event', + target_field: 'event.action', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.uid', + target_field: 'event.id', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.code', + target_field: 'event.code', + ignore_missing: true, + }, + }, + { + date: { + field: 'xdfsfs.ds.@timestamp', + target_field: '@timestamp', + formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'], + tag: 'date_processor_xdfsfs.ds.@timestamp', + if: 'ctx.xdfsfs?.ds?.@timestamp != null', + }, + }, + { + rename: { + field: 'xdfsfs.ds.cluster_name', + target_field: 'cloud.account.name', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.identity.user', + target_field: 'user.name', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.identity.roles', + target_field: 'user.roles', + ignore_missing: true, + }, + }, + { + script: { + description: 'Ensures the date processor does not receive an array value.', + tag: 'script_convert_array_to_string', + lang: 'painless', + source: + 'if (ctx.xdfsfs?.ds?.identity?.expires != null &&\n' + + ' ctx.xdfsfs.ds.identity.expires instanceof ArrayList){\n' + + ' ctx.xdfsfs.ds.identity.expires = ctx.xdfsfs.ds.identity.expires[0];\n' + + '}\n', + }, + }, + { + date: { + field: 'xdfsfs.ds.identity.expires', + target_field: 'user.changes.name', + formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'], + tag: 'date_processor_xdfsfs.ds.identity.expires', + if: 'ctx.xdfsfs?.ds?.identity?.expires != null', + }, + }, + { + convert: { + field: 'xdfsfs.ds.identity.client_ip', + target_field: 'client.ip', + ignore_missing: true, + ignore_failure: true, + type: 'ip', + }, + }, + { + script: { + description: 'Ensures the date processor does not receive an array value.', + tag: 'script_convert_array_to_string', + lang: 'painless', + source: + 'if (ctx.xdfsfs?.ds?.identity?.prev_identity_expires != null &&\n' + + ' ctx.xdfsfs.ds.identity.prev_identity_expires instanceof ArrayList){\n' + + ' ctx.xdfsfs.ds.identity.prev_identity_expires = ctx.xdfsfs.ds.identity.prev_identity_expires[0];\n' + + '}\n', + }, + }, + { + date: { + field: 'xdfsfs.ds.identity.prev_identity_expires', + target_field: 'event.end', + formats: ["yyyy-MM-dd'T'HH:mm:ss'Z'", 'ISO8601'], + tag: 'date_processor_xdfsfs.ds.identity.prev_identity_expires', + if: 'ctx.xdfsfs?.ds?.identity?.prev_identity_expires != null', + }, + }, + { + rename: { + field: 'xdfsfs.ds.user', + target_field: 'user.name', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.login', + target_field: 'user.id', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.server_id', + target_field: 'host.id', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.server_hostname', + target_field: 'host.hostname', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.addr.remote', + target_field: 'source.address', + ignore_missing: true, + }, + }, + { + rename: { + field: 'xdfsfs.ds.proto', + target_field: 'network.protocol', + ignore_missing: true, + }, + }, + { + script: { + description: 'Drops null/empty values recursively.', + tag: 'script_drop_null_empty_values', + lang: 'painless', + source: + 'boolean dropEmptyFields(Object object) {\n' + + ' if (object == null || object == "") {\n' + + ' return true;\n' + + ' } else if (object instanceof Map) {\n' + + ' ((Map) object).values().removeIf(value -> dropEmptyFields(value));\n' + + ' return (((Map) object).size() == 0);\n' + + ' } else if (object instanceof List) {\n' + + ' ((List) object).removeIf(value -> dropEmptyFields(value));\n' + + ' return (((List) object).length == 0);\n' + + ' }\n' + + ' return false;\n' + + '}\n' + + 'dropEmptyFields(ctx);\n', + }, + }, + { + geoip: { + field: 'source.ip', + tag: 'geoip_source_ip', + target_field: 'source.geo', + ignore_missing: true, + }, + }, + { + geoip: { + ignore_missing: true, + database_file: 'GeoLite2-ASN.mmdb', + field: 'source.ip', + tag: 'geoip_source_asn', + target_field: 'source.as', + properties: ['asn', 'organization_name'], + }, + }, + { + rename: { + field: 'source.as.asn', + tag: 'rename_source_as_asn', + target_field: 'source.as.number', + ignore_missing: true, + }, + }, + { + rename: { + field: 'source.as.organization_name', + tag: 'rename_source_as_organization_name', + target_field: 'source.as.organization.name', + ignore_missing: true, + }, + }, + { + geoip: { + field: 'destination.ip', + tag: 'geoip_destination_ip', + target_field: 'destination.geo', + ignore_missing: true, + }, + }, + { + geoip: { + database_file: 'GeoLite2-ASN.mmdb', + field: 'destination.ip', + tag: 'geoip_destination_asn', + target_field: 'destination.as', + properties: ['asn', 'organization_name'], + ignore_missing: true, + }, + }, + { + rename: { + field: 'destination.as.asn', + tag: 'rename_destination_as_asn', + target_field: 'destination.as.number', + ignore_missing: true, + }, + }, + { + rename: { + field: 'destination.as.organization_name', + tag: 'rename_destination_as_organization_name', + target_field: 'destination.as.organization.name', + ignore_missing: true, + }, + }, + { + remove: { + field: ['xdfsfs.ds.identity.client_ip'], + ignore_missing: true, + tag: 'remove_fields', + }, + }, + { + remove: { + field: 'event.original', + tag: 'remove_original_event', + if: 'ctx?.tags == null || !(ctx.tags.contains("preserve_original_event"))', + ignore_failure: true, + ignore_missing: true, + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.ts b/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.ts index b5a6e23000d32..dda48c97bdf98 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/ecs/pipeline.ts @@ -186,6 +186,9 @@ export function createPipeline(state: EcsMappingState): IngestPipeline { const env = new Environment(new FileSystemLoader(templatesPath), { autoescape: false, }); + env.addFilter('includes', function (str, substr) { + return str.includes(substr); + }); env.addFilter('startswith', function (str, prefix) { return str.startsWith(prefix); }); diff --git a/x-pack/plugins/integration_assistant/server/templates/manifest/http_endpoint_manifest.yml.njk b/x-pack/plugins/integration_assistant/server/templates/manifest/http_endpoint_manifest.yml.njk index b62a582a1cfc9..b35471ad4a631 100644 --- a/x-pack/plugins/integration_assistant/server/templates/manifest/http_endpoint_manifest.yml.njk +++ b/x-pack/plugins/integration_assistant/server/templates/manifest/http_endpoint_manifest.yml.njk @@ -42,12 +42,6 @@ The Ingest Node pipeline ID to be used by the integration. required: false show_user: true - - name: preserve_original_event - type: bool - title: Preserve Original Event - description: This option copies the raw unmodified body of the incoming request to the event.original field as a string before sending the event to Elasticsearch. - required: false - show_user: true - name: prefix type: text title: Prefix diff --git a/x-pack/plugins/integration_assistant/server/templates/pipeline.yml.njk b/x-pack/plugins/integration_assistant/server/templates/pipeline.yml.njk index ba846dc50fba9..116d5cc66719f 100644 --- a/x-pack/plugins/integration_assistant/server/templates/pipeline.yml.njk +++ b/x-pack/plugins/integration_assistant/server/templates/pipeline.yml.njk @@ -35,6 +35,7 @@ processors: target_field: {% if value.target_field | startswith('@') %}"{{ value.target_field }}"{% else %}{{ value.target_field }}{% endif %} ignore_missing: true{% endif %} {% if key == 'date' %} + {% if not value.field | includes('.@') %} {# Leaving fields of type 'test.log.@timestamp' #} - script: description: Ensures the date processor does not receive an array value. tag: script_convert_array_to_string @@ -43,7 +44,7 @@ processors: if (ctx.{% endraw %}{{ value.field.replaceAll('.', '?.') }}{% raw %} != null && ctx.{% endraw %}{{ value.field }}{% raw %} instanceof ArrayList){ ctx.{% endraw %}{{ value.field }}{% raw %} = ctx.{% endraw %}{{ value.field }}{% raw %}[0]; - }{% endraw %} + }{% endraw %}{% endif %} - {{ key }}: field: {{ value.field }} target_field: {% if value.target_field | startswith('@') %}"{{ value.target_field }}"{% else %}{{ value.target_field }}{% endif %} diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 955d260abe8a6..b8154c4bc5431 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -10,13 +10,17 @@ import type { RefreshInterval, TimeRange } from '@kbn/data-plugin/common/query'; import type { Filter } from '@kbn/es-query'; export const PLUGIN_ID = 'lens'; -export const APP_ID = 'lens'; -export const LENS_APP_NAME = 'lens'; -export const LENS_EMBEDDABLE_TYPE = 'lens'; +export const APP_ID = PLUGIN_ID; export const DOC_TYPE = 'lens'; +export const LENS_APP_NAME = APP_ID; +export const LENS_EMBEDDABLE_TYPE = DOC_TYPE; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_API_URL = '/api/lens'; export const LENS_EDIT_BY_VALUE = 'edit_by_value'; +export const LENS_ICON = 'lensApp'; +export const STAGE_ID = 'production'; + +export const INDEX_PATTERN_TYPE = 'index-pattern'; export const PieChartTypes = { PIE: 'pie', diff --git a/x-pack/plugins/lens/common/embeddable_factory/index.ts b/x-pack/plugins/lens/common/embeddable_factory/index.ts index 68e6c77e9daeb..62cd68e15e9d1 100644 --- a/x-pack/plugins/lens/common/embeddable_factory/index.ts +++ b/x-pack/plugins/lens/common/embeddable_factory/index.ts @@ -6,47 +6,52 @@ */ import { cloneDeep } from 'lodash'; -import type { SerializableRecord, Serializable } from '@kbn/utility-types'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { SavedObjectReference } from '@kbn/core/types'; -import type { - EmbeddableStateWithType, +import { EmbeddableRegistryDefinition, + EmbeddableStateWithType, } from '@kbn/embeddable-plugin/common'; +import type { LensRuntimeState } from '../../public'; export type LensEmbeddablePersistableState = EmbeddableStateWithType & { attributes: SerializableRecord; }; -export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => { - // We need to clone the state because we can not modify the original state object. - const typedState = cloneDeep(state) as LensEmbeddablePersistableState; +export const inject: NonNullable<EmbeddableRegistryDefinition['inject']> = ( + state, + references +): EmbeddableStateWithType => { + const typedState = cloneDeep(state) as unknown as LensRuntimeState; - if ('attributes' in typedState && typedState.attributes !== undefined) { - // match references based on name, so only references associated with this lens panel are injected. - const matchedReferences: SavedObjectReference[] = []; - - if (Array.isArray(typedState.attributes.references)) { - typedState.attributes.references.forEach((serializableRef) => { - const internalReference = serializableRef as unknown as SavedObjectReference; - const matchedReference = references.find( - (reference) => reference.name === internalReference.name - ); - if (matchedReference) matchedReferences.push(matchedReference); - }); - } - - typedState.attributes.references = matchedReferences as unknown as Serializable[]; + if (typedState.savedObjectId) { + return typedState as unknown as EmbeddableStateWithType; } - return typedState; + // match references based on name, so only references associated with this lens panel are injected. + const matchedReferences: SavedObjectReference[] = []; + + if (Array.isArray(typedState.attributes.references)) { + typedState.attributes.references.forEach((serializableRef) => { + const internalReference = serializableRef; + const matchedReference = references.find( + (reference) => reference.name === internalReference.name + ); + if (matchedReference) matchedReferences.push(matchedReference); + }); + } + + typedState.attributes.references = matchedReferences; + + return typedState as unknown as EmbeddableStateWithType; }; -export const extract: EmbeddableRegistryDefinition['extract'] = (state) => { +export const extract: NonNullable<EmbeddableRegistryDefinition['extract']> = (state) => { let references: SavedObjectReference[] = []; - const typedState = state as LensEmbeddablePersistableState; + const typedState = state as unknown as LensRuntimeState; if ('attributes' in typedState && typedState.attributes !== undefined) { - references = typedState.attributes.references as unknown as SavedObjectReference[]; + references = typedState.attributes.references; } return { state, references }; diff --git a/x-pack/plugins/lens/common/locator/locator.ts b/x-pack/plugins/lens/common/locator/locator.ts index ea0e54136ffc9..7b0b12416f145 100644 --- a/x-pack/plugins/lens/common/locator/locator.ts +++ b/x-pack/plugins/lens/common/locator/locator.ts @@ -9,7 +9,7 @@ import rison from '@kbn/rison'; import type { SerializableRecord } from '@kbn/utility-types'; import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; -import type { Filter, Query } from '@kbn/es-query'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { DataViewSpec, SavedQuery } from '@kbn/data-plugin/common'; import { SavedObjectReference } from '@kbn/core-saved-objects-common'; import type { DateRange } from '../types'; @@ -26,7 +26,7 @@ interface LensShareableState { /** * Optionally set a query. */ - query?: Query; + query?: Query | AggregateQuery; /** * Optionally set the date range in the date picker. @@ -88,7 +88,7 @@ export interface LensAppLocatorParams extends SerializableRecord { /** * Optionally set a query. */ - query?: Query; + query?: Query | AggregateQuery; /** * Optionally set the date range in the date picker. diff --git a/x-pack/plugins/lens/kibana.jsonc b/x-pack/plugins/lens/kibana.jsonc index 4b0b14141474f..012a077abb122 100644 --- a/x-pack/plugins/lens/kibana.jsonc +++ b/x-pack/plugins/lens/kibana.jsonc @@ -45,6 +45,7 @@ "expressionLegacyMetricVis", "expressionPartitionVis", "usageCollection", + "embeddableEnhanced", "taskManager", "globalSearch", "savedObjectsTagging", 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 8aebc4778e201..73fb52bbe6683 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -5,23 +5,20 @@ * 2.0. */ -import React, { PropsWithChildren } from 'react'; +import React from 'react'; import { Observable, 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, SavedObjectIndexStore } from '../persistence'; +import { LensDocument, SavedObjectIndexStore } from '../persistence'; import { visualizationMap, datasourceMap, makeDefaultServices, - mountWithProvider, + renderWithReduxStore, mockStoreDeps, + defaultDoc, } from '../mocks'; -import { I18nProvider } from '@kbn/i18n-react'; -import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public'; import { checkForDuplicateTitle } from '../persistence'; import { createMemoryHistory } from 'history'; import type { Query } from '@kbn/es-query'; @@ -29,55 +26,52 @@ import { FilterManager } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { buildExistsFilter, FilterStateStore } from '@kbn/es-query'; import type { FieldSpec } from '@kbn/data-plugin/common'; -import { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import { LensByValueInput } from '../embeddable/embeddable'; -import { SavedObjectReference } from '@kbn/core/types'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { serverlessMock } from '@kbn/serverless/public/mocks'; +import { cloneDeep } from 'lodash'; import moment from 'moment'; - import { setState, LensAppState } from '../state_management'; import { coreMock } from '@kbn/core/public/mocks'; -jest.mock('../editor_frame_service/editor_frame/expression_helpers'); -jest.mock('@kbn/core/public'); +import { LensSerializedState } from '..'; +import { createMockedField, createMockedIndexPattern } from '../datasources/form_based/mocks'; +import faker from 'faker'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { VisualizeEditorContext } from '../types'; +import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks'; + jest.mock('../persistence/saved_objects_utils/check_for_duplicate_title', () => ({ checkForDuplicateTitle: jest.fn(), })); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn: unknown) => fn, +})); -jest.mock('lodash', () => { - const original = jest.requireActual('lodash'); - - return { - ...original, - debounce: (fn: unknown) => fn, - }; -}); +const defaultSavedObjectId: string = faker.random.uuid(); -// const navigationStartMock = navigationPluginMock.createStartContract(); +const waitToLoad = async () => + await act(async () => new Promise((resolve) => setTimeout(resolve, 0))); -const sessionIdSubject = new Subject<string>(); +function getLensDocumentMock(propsOverrides?: Partial<LensDocument>) { + return cloneDeep({ ...defaultDoc, ...propsOverrides }); +} describe('Lens App', () => { - let defaultDoc: Document; - let defaultSavedObjectId: string; - - function createMockFrame(): jest.Mocked<EditorFrameInstance> { - return { - EditorFrameContainer: jest.fn((props: EditorFrameProps) => <div />), - datasourceMap, - visualizationMap, - }; - } - - const navMenuItems = { - expectedSaveButton: { emphasize: true, testId: 'lnsApp_saveButton' }, - expectedSaveAsButton: { emphasize: false, testId: 'lnsApp_saveButton' }, - expectedSaveAndReturnButton: { emphasize: true, testId: 'lnsApp_saveAndReturnButton' }, - }; + let props: jest.Mocked<LensAppProps>; + let services: jest.Mocked<LensAppServices> = makeDefaultServices( + new Subject<string>(), + 'sessionId-1' + ); + beforeAll(() => setMockedPresentationUtilServices()); - function makeDefaultProps(): jest.Mocked<LensAppProps> { - return { - editorFrame: createMockFrame(), + beforeEach(() => { + props = { + editorFrame: { + EditorFrameContainer: jest.fn((_) => <div>Editor frame</div>), + datasourceMap, + visualizationMap, + }, history: createMemoryHistory(), redirectTo: jest.fn(), redirectToOrigin: jest.fn(), @@ -94,93 +88,60 @@ describe('Lens App', () => { search: jest.fn(), } as unknown as SavedObjectIndexStore, }; - } - const makeDefaultServicesForApp = () => makeDefaultServices(sessionIdSubject, 'sessionId-1'); + services = makeDefaultServices(new Subject<string>(), 'sessionId-1'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); - async function mountWith({ - props = makeDefaultProps(), - services = makeDefaultServicesForApp(), + async function renderApp({ preloadedState, }: { - props?: jest.Mocked<LensAppProps>; - services?: jest.Mocked<LensAppServices>; preloadedState?: Partial<LensAppState>; - }) { - const wrappingComponent: React.FC<PropsWithChildren<{}>> = ({ children }) => { - return ( - <I18nProvider> - <KibanaContextProvider services={services}>{children}</KibanaContextProvider> - </I18nProvider> - ); - }; - const storeDeps = mockStoreDeps({ lensServices: services }); - const { instance, lensStore } = await mountWithProvider( + } = {}) { + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + <KibanaContextProvider services={services}>{children}</KibanaContextProvider> + ); + + const { + store, + render: renderRtl, + rerender, + ...rest + } = renderWithReduxStore( <App {...props} />, + { wrapper: Wrapper }, { - storeDeps, + storeDeps: mockStoreDeps({ lensServices: services }), preloadedState, - }, - { wrappingComponent } + } ); - const frame = props.editorFrame as ReturnType<typeof createMockFrame>; - lensStore.dispatch(setState({ ...preloadedState })); - return { instance, frame, props, services, lensStore }; - } + const rerenderWithProps = (newProps: Partial<LensAppProps>) => { + rerender(<App {...props} {...newProps} />, { + wrapper: Wrapper, + }); + }; - beforeEach(() => { - defaultSavedObjectId = '1234'; - defaultDoc = { - savedObjectId: defaultSavedObjectId, - visualizationType: 'testVis', - type: 'lens', - title: 'An extremely cool default document!', - expression: 'definitely a valid expression', - state: { - query: 'lucene', - filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }], - }, - references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - } as unknown as Document; - }); + await act(async () => await store.dispatch(setState({ ...preloadedState }))); + return { props, lensStore: store, rerender: rerenderWithProps, ...rest }; + } it('renders the editor frame', async () => { - const { frame } = await mountWith({}); - expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith( - { - indexPatternService: expect.any(Object), - getUserMessages: expect.any(Function), - addUserMessages: expect.any(Function), - lensInspector: { - adapters: { - expression: expect.any(Object), - requests: expect.any(Object), - tables: expect.any(Object), - }, - close: expect.any(Function), - inspect: expect.any(Function), - }, - showNoDataPopover: expect.any(Function), - }, - {} - ); + await renderApp(); + expect(screen.getByText('Editor frame')).toBeInTheDocument(); }); it('updates global filters with store state', async () => { - const services = makeDefaultServicesForApp(); - const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView; - const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; + const pinnedField = createMockedField({ name: 'pinnedField', type: '' }); + const indexPattern = createMockedIndexPattern({ id: 'index1' }, [pinnedField]); const pinnedFilter = buildExistsFilter(pinnedField, indexPattern); - services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { - return []; - }); - services.data.query.filterManager.getGlobalFilters = jest.fn().mockImplementation(() => { - return [pinnedFilter]; - }); - const { instance, lensStore } = await mountWith({ services }); + services.data.query.filterManager.getFilters = jest.fn().mockReturnValue([]); + services.data.query.filterManager.getGlobalFilters = jest.fn().mockReturnValue([pinnedFilter]); + const { lensStore } = await renderApp(); - instance.update(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ query: { query: '', language: 'lucene' }, @@ -198,22 +159,19 @@ describe('Lens App', () => { describe('extra nav menu entries', () => { it('shows custom menu entry', async () => { const runFn = jest.fn(); - const { instance, services } = await mountWith({ - props: { - ...makeDefaultProps(), - topNavMenuEntryGenerators: [ - () => ({ - label: 'My entry', - run: runFn, - }), - ], - }, - }); - const navigationComponent = services.navigation.ui - .AggregateQueryTopNavMenu as unknown as React.ReactElement; - const extraEntry = instance.find(navigationComponent).prop('config')[0]; - expect(extraEntry.label).toEqual('My entry'); - expect(extraEntry.run).toBe(runFn); + props.topNavMenuEntryGenerators = [ + () => ({ + label: 'My entry', + run: runFn, + }), + ]; + await renderApp(); + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.arrayContaining([{ label: 'My entry', run: runFn }]), + }), + {} + ); }); it('passes current state, filter, query timerange and initial context into getter', async () => { @@ -244,15 +202,12 @@ describe('Lens App', () => { }, ], }; - await mountWith({ - props: { - ...makeDefaultProps(), - topNavMenuEntryGenerators: [getterFn], - initialContext: { - fieldName: 'a', - dataViewSpec: { id: '1' }, - }, - }, + props.topNavMenuEntryGenerators = [getterFn]; + props.initialContext = { + fieldName: 'a', + dataViewSpec: { id: '1' }, + }; + await renderApp({ preloadedState, }); @@ -278,19 +233,14 @@ describe('Lens App', () => { }); describe('breadcrumbs', () => { - const breadcrumbDocSavedObjectId = defaultSavedObjectId; - const breadcrumbDoc = { + const breadcrumbDocSavedObjectId = faker.random.uuid(); + const breadcrumbDoc = getLensDocumentMock({ savedObjectId: breadcrumbDocSavedObjectId, title: 'Daaaaaaadaumching!', - state: { - query: 'fake query', - filters: [], - }, - references: [], - } as unknown as Document; + }); it('sets breadcrumbs when the document title changes', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { @@ -302,8 +252,7 @@ describe('Lens App', () => { ]); await act(async () => { - instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); - lensStore.dispatch( + await lensStore.dispatch( setState({ persistedDoc: breadcrumbDoc, }) @@ -321,17 +270,10 @@ describe('Lens App', () => { }); it('sets originatingApp breadcrumb when the document title changes', async () => { - const props = makeDefaultProps(); - const services = makeDefaultServicesForApp(); - props.incomingState = { originatingApp: 'coolContainer' }; + props.incomingState = { originatingApp: 'dashboards' }; services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made'); - - const { instance, lensStore } = await mountWith({ - props, - services, - preloadedState: { - isLinkedToOriginatingApp: false, - }, + const { lensStore, rerender } = await renderApp({ + preloadedState: { isLinkedToOriginatingApp: false }, }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -344,12 +286,7 @@ describe('Lens App', () => { ]); await act(async () => { - instance.setProps({ - initialInput: { savedObjectId: breadcrumbDocSavedObjectId }, - preloadedState: { - isLinkedToOriginatingApp: true, - }, - }); + await rerender({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); lensStore.dispatch( setState({ @@ -370,17 +307,13 @@ describe('Lens App', () => { it('sets serverless breadcrumbs when the document title changes when serverless service is available', async () => { const serverless = serverlessMock.createStart(); - const { instance, services, lensStore } = await mountWith({ - services: { - ...makeDefaultServices(), - serverless, - }, - }); + services.serverless = serverless; + const { lensStore, rerender } = await renderApp(); expect(services.chrome.setBreadcrumbs).not.toHaveBeenCalled(); expect(serverless.setBreadcrumbs).toHaveBeenCalledWith({ text: 'Create' }); await act(async () => { - instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + rerender({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); lensStore.dispatch( setState({ persistedDoc: breadcrumbDoc, @@ -395,43 +328,40 @@ describe('Lens App', () => { describe('TopNavMenu#showDatePicker', () => { it('shows date picker if any used index pattern isTimeBased', async () => { - const customServices = makeDefaultServicesForApp(); - customServices.dataViews.get = jest + services.dataViews.get = jest .fn() - .mockImplementation((id) => - Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) + .mockImplementation( + async (id) => ({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) ); - const { services } = await mountWith({ services: customServices }); + await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showDatePicker: true }), {} ); }); it('shows date picker if active datasource isTimeBased', async () => { - const customServices = makeDefaultServicesForApp(); - customServices.dataViews.get = jest + services.dataViews.get = jest .fn() - .mockImplementation((id) => - Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) + .mockImplementation( + async (id) => ({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) ); - const customProps = makeDefaultProps(); - customProps.datasourceMap.testDatasource.isTimeBased = () => true; - const { services } = await mountWith({ props: customProps, services: customServices }); + + props.datasourceMap.testDatasource.isTimeBased = () => true; + await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showDatePicker: true }), {} ); }); it('does not show date picker if index pattern nor active datasource is not time based', async () => { - const customServices = makeDefaultServicesForApp(); - customServices.dataViews.get = jest + services.dataViews.get = jest .fn() - .mockImplementation((id) => - Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) + .mockImplementation( + async (id) => ({ id, isTimeBased: () => true, isPersisted: () => true } as DataView) ); - const customProps = makeDefaultProps(); - customProps.datasourceMap.testDatasource.isTimeBased = () => false; - const { services } = await mountWith({ props: customProps, services: customServices }); + + props.datasourceMap.testDatasource.isTimeBased = () => false; + await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showDatePicker: false }), {} @@ -441,7 +371,7 @@ describe('Lens App', () => { describe('TopNavMenu#dataViewPickerProps', () => { it('calls the nav component with the correct dataview picker props if permissions are given', async () => { - const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); + const { lensStore } = await renderApp(); services.dataViewEditor.userPermissions.editDataView = () => true; const document = { savedObjectId: defaultSavedObjectId, @@ -450,8 +380,9 @@ describe('Lens App', () => { filters: [{ query: { match_phrase: { src: 'test' } } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - } as unknown as Document; + } as unknown as LensDocument; + (services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock).mockClear(); act(() => { lensStore.dispatch( setState({ @@ -460,46 +391,44 @@ describe('Lens App', () => { }) ); }); - instance.update(); - const props = instance - .find('[data-test-subj="lnsApp_topNav"]') - .prop('dataViewPickerComponentProps') as TopNavMenuData[]; - expect(props).toEqual( + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - currentDataViewId: 'mockip', - onChangeDataView: expect.any(Function), - onDataViewCreated: expect.any(Function), - onAddField: expect.any(Function), - }) + dataViewPickerComponentProps: expect.objectContaining({ + currentDataViewId: 'mockip', + onChangeDataView: expect.any(Function), + onDataViewCreated: expect.any(Function), + onAddField: expect.any(Function), + }), + }), + {} ); }); }); describe('persistence', () => { it('passes query and indexPatterns to TopNavMenu', async () => { - const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); - const document = { + const { lensStore } = await renderApp(); + const query = { query: 'fake query', language: 'kuery' }; + const document = getLensDocumentMock({ savedObjectId: defaultSavedObjectId, state: { - query: 'fake query', - filters: [{ query: { match_phrase: { src: 'test' } } }], + ...defaultDoc.state, + query, + filters: [{ query: { match_phrase: { src: 'test' } }, meta: {} }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - } as unknown as Document; - - act(() => { - lensStore.dispatch( - setState({ - query: 'fake query' as unknown as Query, - persistedDoc: document, - }) - ); }); - instance.update(); + + await lensStore.dispatch( + setState({ + query, + persistedDoc: document, + }) + ); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - query: 'fake query', + query, indexPatterns: [ { id: 'mockip', @@ -514,240 +443,155 @@ describe('Lens App', () => { ); }); it('handles rejected index pattern', async () => { - const customServices = makeDefaultServicesForApp(); - customServices.dataViews.get = jest + services.dataViews.get = jest .fn() - .mockImplementation((id) => Promise.reject({ reason: 'Could not locate that data view' })); - const customProps = makeDefaultProps(); - const { services } = await mountWith({ props: customProps, services: customServices }); + .mockResolvedValue(Promise.reject({ reason: 'Could not locate that data view' })); + await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [] }), {} ); }); - describe('save buttons', () => { - interface SaveProps { - newCopyOnSave: boolean; - returnToOrigin?: boolean; - newTitle: string; - } - function getButton(inst: ReactWrapper): TopNavMenuData { - return ( - inst.find('[data-test-subj="lnsApp_topNav"]').prop('config') as TopNavMenuData[] - ).find((button) => button.testId === 'lnsApp_saveButton')!; - } + describe('save buttons', () => { + const querySaveButton = () => screen.queryByTestId('lnsApp_saveButton'); + const clickSaveButton = async () => + await act(async () => await userEvent.click(screen.getByTestId('lnsApp_saveButton'))); - async function testSave(inst: ReactWrapper, saveProps: SaveProps) { - getButton(inst).run(inst.getDOMNode()); - // wait a tick since SaveModalContainer initializes asynchronously - await new Promise(process.nextTick); - const handler = inst.update().find('SavedObjectSaveModalOrigin').prop('onSave') as ( - p: unknown - ) => void; - handler(saveProps); - } + const querySaveAndReturnButton = () => screen.queryByTestId('lnsApp_saveAndReturnButton'); + const waitForModalVisible = async () => + await waitFor(() => screen.getByTestId('savedObjectTitle')); async function save({ preloadedState, - initialSavedObjectId, - ...saveProps - }: SaveProps & { + savedObjectId = defaultSavedObjectId, + prevSavedObjectId = undefined, + newTitle = 'hello there', + newCopyOnSave = false, + comesFromDashboard = true, + switchToAddToDashboardNone = false, + }: { + newCopyOnSave?: boolean; + newTitle?: string; preloadedState?: Partial<LensAppState>; - initialSavedObjectId?: string; + prevSavedObjectId?: string; + savedObjectId?: string; + comesFromDashboard?: boolean; + switchToAddToDashboardNone?: boolean; }) { - const props = { - ...makeDefaultProps(), - initialInput: initialSavedObjectId - ? { savedObjectId: initialSavedObjectId, id: '5678' } - : undefined, - }; - - props.incomingState = { - originatingApp: 'ultraDashboard', - }; - - const services = makeDefaultServicesForApp(); - services.attributeService.wrapAttributes = jest - .fn() - .mockImplementation(async ({ savedObjectId }) => ({ - savedObjectId: savedObjectId || 'aaa', - })); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ - metaInfo: { - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, + services.attributeService.saveToLibrary = jest.fn().mockResolvedValue(savedObjectId); + services.attributeService.loadFromLibrary = jest.fn().mockResolvedValue({ + sharingSavedObjectProps: { + outcome: 'exactMatch', }, attributes: { - savedObjectId: initialSavedObjectId ?? 'aaa', + savedObjectId, references: [], state: { - query: 'fake query', + query: { query: 'fake query', language: 'kuery' }, filters: [], }, }, - } as jest.ResolvedValue<Document>); + managed: false, + }); + + props = { + ...props, + initialInput: prevSavedObjectId ? { savedObjectId: prevSavedObjectId } : undefined, + }; - const { frame, instance, lensStore } = await mountWith({ - services, - props, + if (comesFromDashboard) { + props.incomingState = { originatingApp: 'dashboards' }; + } + + const { lensStore } = await renderApp({ preloadedState: { isSaveable: true, - isLinkedToOriginatingApp: true, + isLinkedToOriginatingApp: comesFromDashboard, ...preloadedState, }, }); - expect(getButton(instance).disableButton).toEqual(false); - await act(async () => { - testSave(instance, { ...saveProps }); + await clickSaveButton(); + await waitForModalVisible(); + if (newCopyOnSave) { + await userEvent.click(screen.getByTestId('saveAsNewCheckbox')); + } + if (switchToAddToDashboardNone) { + await userEvent.click(screen.getByLabelText('None')); + } + await waitFor(async () => { + await userEvent.clear(screen.getByTestId('savedObjectTitle')); + expect(screen.getByTestId('savedObjectTitle')).toHaveValue(''); }); - return { props, services, instance, frame, lensStore }; + await userEvent.type(screen.getByTestId('savedObjectTitle'), `${newTitle}`); + await userEvent.click(screen.getByTestId('confirmSaveSavedObjectButton')); + await waitToLoad(); + return { props, lensStore }; } it('shows a disabled save button when the user does not have permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: false, saveQuery: false, show: true }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, + dashboard: { + showWriteControls: false, }, }; - const { instance, lensStore } = await mountWith({ services }); - expect(getButton(instance).disableButton).toEqual(true); - act(() => { - lensStore.dispatch( - setState({ - isSaveable: true, - }) - ); - }); - instance.update(); - expect(getButton(instance).disableButton).toEqual(true); + await renderApp({ preloadedState: { isSaveable: true } }); + expect(querySaveButton()).toBeDisabled(); }); 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 { instance, lensStore, services } = await mountWith({}); - expect(getButton(instance).disableButton).toEqual(true); - act(() => { - lensStore.dispatch( - setState({ - isSaveable: true, - }) - ); + await renderApp({ + preloadedState: { isSaveable: true }, }); - instance.update(); - expect(getButton(instance).disableButton).toEqual(false); - await act(async () => { - const topNavMenuConfig = instance - .find(services.navigation.ui.AggregateQueryTopNavMenu) - .prop('config'); - expect(topNavMenuConfig).not.toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) - ); - expect(topNavMenuConfig).not.toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAsButton) - ); - expect(topNavMenuConfig).toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveButton) - ); - }); + expect(querySaveButton()).toHaveTextContent('Save'); + expect(querySaveAndReturnButton()).toBeFalsy(); }); - it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => { - const props = makeDefaultProps(); - const services = makeDefaultServicesForApp(); + it('Shows Save and Return and Save to library buttons in create by value mode with originating app', async () => { props.incomingState = { - originatingApp: 'ultraDashboard', + originatingApp: 'dashboards', valueInput: { id: 'whatchaGonnaDoWith', attributes: { title: 'whatcha gonna do with all these references? All these references in your value Input', - references: [] as SavedObjectReference[], + references: [], }, - } as LensByValueInput, + } as unknown as LensSerializedState, }; - - const { instance } = await mountWith({ - props, - services, + await renderApp({ preloadedState: { isLinkedToOriginatingApp: true, + isSaveable: true, }, }); - await act(async () => { - const topNavMenuConfig = instance - .find(services.navigation.ui.AggregateQueryTopNavMenu) - .prop('config'); - expect(topNavMenuConfig).toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) - ); - expect(topNavMenuConfig).toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAsButton) - ); - expect(topNavMenuConfig).not.toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveButton) - ); - }); + expect(querySaveAndReturnButton()).toBeEnabled(); + expect(querySaveButton()).toHaveTextContent('Save to library'); }); it('Shows Save and Return and Save As buttons in edit by reference mode', async () => { - const props = makeDefaultProps(); - props.initialInput = { savedObjectId: defaultSavedObjectId, id: '5678' }; props.incomingState = { - originatingApp: 'ultraDashboard', + originatingApp: 'dashboards', }; - - const { instance, services } = await mountWith({ - props, + props.initialInput = { savedObjectId: defaultSavedObjectId, id: '5678' }; + await renderApp({ preloadedState: { + isSaveable: true, isLinkedToOriginatingApp: true, }, }); - await act(async () => { - const topNavMenuConfig = instance - .find(services.navigation.ui.AggregateQueryTopNavMenu) - .prop('config'); - expect(topNavMenuConfig).toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) - ); - expect(topNavMenuConfig).toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveAsButton) - ); - expect(topNavMenuConfig).not.toContainEqual( - expect.objectContaining(navMenuItems.expectedSaveButton) - ); - }); - }); - - it('saves new docs', async () => { - const { props, services } = await save({ - initialSavedObjectId: undefined, - newCopyOnSave: false, - newTitle: 'hello there', - }); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( - expect.objectContaining({ - savedObjectId: undefined, - title: 'hello there', - }), - true, - undefined - ); - expect(props.redirectTo).toHaveBeenCalledWith('aaa'); - expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( - "Saved 'hello there'" - ); + expect(querySaveAndReturnButton()).toBeEnabled(); + expect(querySaveButton()).toHaveTextContent('Save as'); }); it('applies all changes on-save', async () => { const { lensStore } = await save({ - initialSavedObjectId: undefined, + savedObjectId: undefined, newCopyOnSave: false, newTitle: 'hello there', preloadedState: { @@ -756,120 +600,91 @@ describe('Lens App', () => { }); expect(lensStore.getState().lens.applyChangesCounter).toBe(1); }); - it('adds to the recently accessed list on save', async () => { - const { services } = await save({ - initialSavedObjectId: undefined, - newCopyOnSave: false, - newTitle: 'hello there', - }); + const savedObjectId = faker.random.uuid(); + await save({ savedObjectId, prevSavedObjectId: 'prevId', comesFromDashboard: false }); expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( - '/app/lens#/edit/aaa', + `/app/lens#/edit/${savedObjectId}`, 'hello there', - 'aaa' + savedObjectId ); }); - it('saves the latest doc as a copy', async () => { - const { props, services, instance } = await save({ - initialSavedObjectId: defaultSavedObjectId, - newCopyOnSave: true, + it('saves new docs', async () => { + await save({ + prevSavedObjectId: undefined, + savedObjectId: defaultSavedObjectId, newTitle: 'hello there', - preloadedState: { persistedDoc: defaultDoc }, + comesFromDashboard: false, + switchToAddToDashboardNone: true, }); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( + expect(services.attributeService.saveToLibrary).toHaveBeenCalledWith( expect.objectContaining({ title: 'hello there', }), - true, + // from mocks + [ + { + id: 'mockip', + name: 'mockip', + type: 'index-pattern', + }, + ], undefined ); expect(props.redirectTo).toHaveBeenCalledWith(defaultSavedObjectId); - await act(async () => { - instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledTimes(1); expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( "Saved 'hello there'" ); }); - it('saves existing docs', async () => { - const { props, services, instance } = await save({ - initialSavedObjectId: defaultSavedObjectId, - newCopyOnSave: false, + it('saves existing docs as a copy', async () => { + const doc = getLensDocumentMock(); + await save({ + savedObjectId: doc.savedObjectId, + newCopyOnSave: true, newTitle: 'hello there', - preloadedState: { persistedDoc: defaultDoc }, + preloadedState: { persistedDoc: doc }, + prevSavedObjectId: 'prevId', + comesFromDashboard: false, }); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( + expect(services.attributeService.saveToLibrary).toHaveBeenCalledWith( expect.objectContaining({ - savedObjectId: defaultSavedObjectId, title: 'hello there', }), - true, - { id: '5678', savedObjectId: defaultSavedObjectId } + [{ id: 'mockip', name: 'mockip', type: 'index-pattern' }], + undefined ); - expect(props.redirectTo).not.toHaveBeenCalled(); - await act(async () => { - instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); + // new copy gets a new SO id + expect(props.redirectTo).toHaveBeenCalledWith(doc.savedObjectId); + expect(services.attributeService.saveToLibrary).toHaveBeenCalledTimes(1); expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( "Saved 'hello there'" ); }); - it('handles save failure by showing a warning, but still allows another save', async () => { - const mockedConsoleDir = jest.spyOn(console, 'dir'); // mocked console.dir to avoid messages in the console when running tests - mockedConsoleDir.mockImplementation(() => {}); - - const props = makeDefaultProps(); - - props.incomingState = { - originatingApp: 'ultraDashboard', - }; - - const services = makeDefaultServicesForApp(); - services.attributeService.wrapAttributes = jest - .fn() - .mockRejectedValue({ message: 'failed' }); - const { instance } = await mountWith({ - props, - services, - preloadedState: { - isSaveable: true, - isLinkedToOriginatingApp: true, - }, - }); - - await act(async () => { - testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' }); - }); - expect(props.redirectTo).not.toHaveBeenCalled(); - expect(getButton(instance).disableButton).toEqual(false); - // eslint-disable-next-line no-console - expect(console.dir).toHaveBeenCalledTimes(1); - mockedConsoleDir.mockRestore(); - }); - - it('saves new doc and redirects to originating app', async () => { - const { props, services } = await save({ - initialSavedObjectId: undefined, - returnToOrigin: true, + it('saves existing docs', async () => { + await save({ + savedObjectId: defaultSavedObjectId, + prevSavedObjectId: defaultSavedObjectId, newCopyOnSave: false, newTitle: 'hello there', + comesFromDashboard: false, + preloadedState: { + persistedDoc: getLensDocumentMock({ savedObjectId: defaultSavedObjectId }), + }, }); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( + expect(services.attributeService.saveToLibrary).toHaveBeenCalledWith( expect.objectContaining({ - savedObjectId: undefined, title: 'hello there', }), - true, - undefined + [{ id: 'mockip', name: 'mockip', type: 'index-pattern' }], + defaultSavedObjectId + ); + expect(props.redirectTo).not.toHaveBeenCalled(); + expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( + "Saved 'hello there'" ); - expect(props.redirectToOrigin).toHaveBeenCalledWith({ - input: { savedObjectId: 'aaa' }, - isCopied: false, - }); }); it('saves app filters and does not save pinned filters', async () => { @@ -881,229 +696,227 @@ describe('Lens App', () => { await act(async () => { FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); }); - const { services } = await save({ - initialSavedObjectId: defaultSavedObjectId, - newCopyOnSave: false, - newTitle: 'hello there2', + + await save({ + savedObjectId: defaultSavedObjectId, + prevSavedObjectId: defaultSavedObjectId, preloadedState: { - persistedDoc: defaultDoc, + isSaveable: true, + persistedDoc: getLensDocumentMock({ savedObjectId: defaultSavedObjectId }), + isLinkedToOriginatingApp: true, filters: [pinned, unpinned], }, }); const { state: expectedFilters } = services.data.query.filterManager.extract([unpinned]); - expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( + expect(services.attributeService.saveToLibrary).toHaveBeenCalledWith( expect.objectContaining({ - savedObjectId: defaultSavedObjectId, - title: 'hello there2', + title: 'hello there', state: expect.objectContaining({ filters: expectedFilters }), }), - true, - { id: '5678', savedObjectId: defaultSavedObjectId } + [{ id: 'mockip', name: 'mockip', type: 'index-pattern' }], + undefined ); }); it('checks for duplicate title before saving', async () => { - const props = makeDefaultProps(); - props.incomingState = { originatingApp: 'coolContainer' }; - const services = makeDefaultServicesForApp(); - services.attributeService.wrapAttributes = jest - .fn() - .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); - const { instance } = await mountWith({ - props, - services, + await save({ + savedObjectId: defaultSavedObjectId, + prevSavedObjectId: defaultSavedObjectId, preloadedState: { isSaveable: true, - persistedDoc: { savedObjectId: '123' } as unknown as Document, + persistedDoc: { savedObjectId: defaultSavedObjectId } as unknown as LensDocument, isLinkedToOriginatingApp: true, }, }); - await act(async () => { - instance.setProps({ initialInput: { savedObjectId: '123' } }); - getButton(instance).run(instance.getDOMNode()); - }); - instance.update(); - const onTitleDuplicate = jest.fn(); - await act(async () => { - instance.find(SavedObjectSaveModal).prop('onSave')({ - onTitleDuplicate, - isTitleDuplicateConfirmed: false, - newCopyOnSave: false, - newDescription: '', - newTitle: 'test', - }); - }); + expect(checkForDuplicateTitle).toHaveBeenCalledWith( - expect.objectContaining({ id: '123', isTitleDuplicateConfirmed: false }), - onTitleDuplicate, + { + copyOnSave: true, + displayName: 'Lens visualization', + isTitleDuplicateConfirmed: false, + lastSavedTitle: '', + title: 'hello there', + }, + expect.any(Function), expect.anything() ); }); + it('saves new doc and redirects to originating app', async () => { + await save({ + savedObjectId: undefined, + newCopyOnSave: false, + newTitle: 'hello there', + }); + expect(services.attributeService.saveToLibrary).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'hello there', + }), + [{ id: 'mockip', name: 'mockip', type: 'index-pattern' }], + undefined + ); + expect(props.redirectToOrigin).toHaveBeenCalledWith({ + state: expect.objectContaining({ savedObjectId: defaultSavedObjectId }), + isCopied: false, + }); + }); + + it('handles save failure by showing a warning, but still allows another save', async () => { + const mockedConsoleDir = jest.spyOn(console, 'dir').mockImplementation(() => {}); // mocked console.dir to avoid messages in the console when running tests + + services.attributeService.saveToLibrary = jest + .fn() + .mockRejectedValue({ message: 'failed' }); + + props.incomingState = { + originatingApp: 'dashboards', + }; + + await renderApp({ + preloadedState: { + isSaveable: true, + isLinkedToOriginatingApp: true, + }, + }); + await clickSaveButton(); + await userEvent.type(screen.getByTestId('savedObjectTitle'), 'hello there'); + await userEvent.click(screen.getByTestId('confirmSaveSavedObjectButton')); + await waitToLoad(); + + expect(props.redirectTo).not.toHaveBeenCalled(); + expect(services.attributeService.saveToLibrary).toHaveBeenCalled(); + // eslint-disable-next-line no-console + expect(console.dir).toHaveBeenCalledTimes(1); + mockedConsoleDir.mockRestore(); + }); + it('does not show the copy button on first save', async () => { - const props = makeDefaultProps(); - props.incomingState = { originatingApp: 'coolContainer' }; - const { instance } = await mountWith({ - props, + props.incomingState = { + originatingApp: 'dashboards', + }; + + await renderApp({ preloadedState: { isSaveable: true, isLinkedToOriginatingApp: true }, }); - await act(async () => getButton(instance).run(instance.getDOMNode())); - instance.update(); - expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + await clickSaveButton(); + await waitForModalVisible(); + expect(screen.queryByTestId('saveAsNewCheckbox')).not.toBeInTheDocument(); }); it('enables Save Query UI when user has app-level permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { saveQuery: true }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { saveQuery: true }, }; - const { instance } = await mountWith({ services }); - await act(async () => { - const topNavMenu = instance.find(services.navigation.ui.AggregateQueryTopNavMenu); - expect(topNavMenu.props().saveQueryMenuVisibility).toBe('allowed_by_app_privilege'); - }); + + await renderApp(); + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenLastCalledWith( + expect.objectContaining({ saveQueryMenuVisibility: 'allowed_by_app_privilege' }), + {} + ); }); it('checks global save query permission when user does not have app-level permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { saveQuery: false }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { saveQuery: false }, }; - const { instance } = await mountWith({ services }); - await act(async () => { - const topNavMenu = instance.find(services.navigation.ui.AggregateQueryTopNavMenu); - expect(topNavMenu.props().saveQueryMenuVisibility).toBe('globally_managed'); - }); + await renderApp(); + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenLastCalledWith( + expect.objectContaining({ saveQueryMenuVisibility: 'globally_managed' }), + {} + ); }); }); }); describe('share button', () => { - function getShareButton(inst: ReactWrapper): TopNavMenuData { - return ( - inst.find('[data-test-subj="lnsApp_topNav"]').prop('config') as TopNavMenuData[] - ).find((button) => button.testId === 'lnsApp_shareButton')!; - } + const getShareButton = () => screen.getByTestId('lnsApp_shareButton'); it('should be disabled when no data is available', async () => { - const { instance } = await mountWith({ preloadedState: { isSaveable: true } }); - expect(getShareButton(instance).disableButton).toEqual(true); + await renderApp({ preloadedState: { isSaveable: true } }); + expect(getShareButton()).toBeDisabled(); }); it('should not disable share when not saveable', async () => { - const { instance } = await mountWith({ + await renderApp({ preloadedState: { isSaveable: false, activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, }, }); - expect(getShareButton(instance).disableButton).toEqual(false); + expect(getShareButton()).toBeEnabled(); }); it('should still be enabled even if the user is missing save permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: false, saveQuery: false, show: true, createShortUrl: true }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true, createShortUrl: true }, }; - const { instance } = await mountWith({ - services, + await renderApp({ preloadedState: { isSaveable: true, activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, }, }); - expect(getShareButton(instance).disableButton).toEqual(false); + expect(getShareButton()).toBeEnabled(); }); it('should still be enabled even if the user is missing shortUrl permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: true, saveQuery: false, show: true, createShortUrl: false }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: true, saveQuery: false, show: true, createShortUrl: false }, }; - const { instance } = await mountWith({ - services, + await renderApp({ preloadedState: { isSaveable: true, activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, }, }); - expect(getShareButton(instance).disableButton).toEqual(false); + + expect(getShareButton()).toBeEnabled(); }); it('should be disabled if the user is missing shortUrl permissions and visualization is not saveable', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: false, saveQuery: false, show: true, createShortUrl: false }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true, createShortUrl: false }, }; - const { instance } = await mountWith({ - services, + await renderApp({ preloadedState: { isSaveable: false, activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, }, }); - expect(getShareButton(instance).disableButton).toEqual(true); + expect(getShareButton()).toBeDisabled(); }); }); describe('inspector', () => { - function getButton(inst: ReactWrapper): TopNavMenuData { - return ( - inst.find('[data-test-subj="lnsApp_topNav"]').prop('config') as TopNavMenuData[] - ).find((button) => button.testId === 'lnsApp_inspectButton')!; - } - - async function runInspect(inst: ReactWrapper) { - await getButton(inst).run(inst.getDOMNode()); - await inst.update(); - } - it('inspector button should be available', async () => { - const { instance } = await mountWith({ preloadedState: { isSaveable: true } }); - const button = getButton(instance); - - expect(button.disableButton).toEqual(false); + await renderApp({ + preloadedState: { isSaveable: true }, + }); + expect(screen.getByTestId('lnsApp_inspectButton')).toBeEnabled(); }); - it('should open inspect panel', async () => { - const services = makeDefaultServicesForApp(); - const { instance } = await mountWith({ services, preloadedState: { isSaveable: true } }); - - await runInspect(instance); - + await renderApp({ + preloadedState: { isSaveable: true }, + }); + await userEvent.click(screen.getByTestId('lnsApp_inspectButton')); expect(services.inspector.inspect).toHaveBeenCalledTimes(1); }); }); describe('query bar state management', () => { it('uses the default time and query language settings', async () => { - const { lensStore, services } = await mountWith({}); + const { lensStore } = await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: '', language: 'lucene' }, @@ -1125,18 +938,20 @@ describe('Lens App', () => { }); it('updates the editor frame when the user changes query or time in the search bar', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); (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'), }); + const onQuerySubmit = (services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock).mock + .calls[0][0].onQuerySubmit; await act(async () => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - instance.update(); + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'new', language: 'lucene' }, @@ -1162,7 +977,7 @@ describe('Lens App', () => { }); it('updates the filters when the user changes them', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView; const field = { name: 'myfield' } as unknown as FieldSpec; expect(lensStore.getState()).toEqual({ @@ -1170,10 +985,9 @@ describe('Lens App', () => { filters: [], }), }); - act(() => - services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) - ); - instance.update(); + + services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]); + expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ filters: [buildExistsFilter(field, indexPattern)], @@ -1182,7 +996,7 @@ describe('Lens App', () => { }); it('updates the searchSessionId when the user changes query or time in the search bar', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ @@ -1190,13 +1004,14 @@ describe('Lens App', () => { }), }); + const AggregateQueryTopNavMenu = services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock; + const onQuerySubmit = AggregateQueryTopNavMenu.mock.calls[0][0].onQuerySubmit; act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: '', language: 'lucene' }, }) ); - instance.update(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ @@ -1205,12 +1020,12 @@ describe('Lens App', () => { }); // trigger again, this time changing just the query act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - instance.update(); + expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `sessionId-3`, @@ -1221,7 +1036,7 @@ describe('Lens App', () => { act(() => services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) ); - instance.update(); + expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `sessionId-4`, @@ -1232,15 +1047,11 @@ describe('Lens App', () => { describe('saved query handling', () => { it('does not allow saving when the user is missing the saveQuery permission', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: false, saveQuery: false, show: true }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, }; - await mountWith({ services }); + await renderApp(); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ saveQueryMenuVisibility: 'globally_managed' }), {} @@ -1248,7 +1059,8 @@ describe('Lens App', () => { }); it('persists the saved query ID when the query is saved', async () => { - const { instance, services } = await mountWith({}); + await renderApp(); + expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ saveQueryMenuVisibility: 'allowed_by_app_privilege', @@ -1259,8 +1071,11 @@ describe('Lens App', () => { }), {} ); + + const onSaved = (services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock).mock + .calls[0][0].onSaved; act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSaved')!({ + onSaved({ id: '1', attributes: { title: '', @@ -1287,9 +1102,12 @@ describe('Lens App', () => { }); it('changes the saved query ID when the query is updated', async () => { - const { instance, services } = await mountWith({}); + await renderApp(); + const { onSaved, onSavedQueryUpdated } = ( + services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock + ).mock.calls[0][0]; act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSaved')!({ + onSaved({ id: '1', attributes: { title: '', @@ -1300,17 +1118,15 @@ describe('Lens App', () => { }); }); act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSavedQueryUpdated')!( - { - id: '2', - attributes: { - title: 'new title', - description: '', - query: { query: '', language: 'lucene' }, - }, - namespaces: ['default'], - } - ); + onSavedQueryUpdated({ + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: '', language: 'lucene' }, + }, + namespaces: ['default'], + }); }); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ @@ -1329,19 +1145,19 @@ describe('Lens App', () => { }); it('updates the query if saved query is selected', async () => { - const { instance, services } = await mountWith({}); + await renderApp(); + const { onSavedQueryUpdated } = (services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock) + .mock.calls[0][0]; act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSavedQueryUpdated')!( - { - id: '2', - attributes: { - title: 'new title', - description: '', - query: { query: 'abc:def', language: 'lucene' }, - }, - namespaces: ['default'], - } - ); + onSavedQueryUpdated({ + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: 'abc:def', language: 'lucene' }, + }, + namespaces: ['default'], + }); }); expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ @@ -1352,9 +1168,12 @@ describe('Lens App', () => { }); it('clears all existing unpinned filters when the active saved query is cleared', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); + const { onQuerySubmit, onClearSavedQuery } = ( + services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock + ).mock.calls[0][0]; act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1366,11 +1185,7 @@ describe('Lens App', () => { const pinned = buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - instance.update(); - act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onClearSavedQuery')!() - ); - instance.update(); + act(() => onClearSavedQuery()); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ filters: [pinned], @@ -1381,9 +1196,12 @@ describe('Lens App', () => { describe('search session id management', () => { it('updates the searchSessionId when the query is updated', async () => { - const { instance, lensStore, services } = await mountWith({}); + const { lensStore } = await renderApp(); + const { onSaved, onSavedQueryUpdated } = ( + services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock + ).mock.calls[0][0]; act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSaved')!({ + onSaved({ id: '1', attributes: { title: '', @@ -1394,19 +1212,16 @@ describe('Lens App', () => { }); }); act(() => { - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onSavedQueryUpdated')!( - { - id: '2', - attributes: { - title: 'new title', - description: '', - query: { query: '', language: 'lucene' }, - }, - namespaces: ['default'], - } - ); + onSavedQueryUpdated({ + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: '', language: 'lucene' }, + }, + namespaces: ['default'], + }); }); - instance.update(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `sessionId-2`, @@ -1415,9 +1230,12 @@ describe('Lens App', () => { }); it('updates the searchSessionId when the active saved query is cleared', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); + const { onQuerySubmit, onClearSavedQuery } = ( + services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock + ).mock.calls[0][0]; act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1429,11 +1247,7 @@ describe('Lens App', () => { const pinned = buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - instance.update(); - act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onClearSavedQuery')!() - ); - instance.update(); + act(() => onClearSavedQuery()); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `sessionId-4`, @@ -1442,14 +1256,14 @@ describe('Lens App', () => { }); it('dispatches update to searchSessionId and dateRange when the user hits refresh', async () => { - const { instance, services, lensStore } = await mountWith({}); + const { lensStore } = await renderApp(); + const { onQuerySubmit } = (services.navigation.ui.AggregateQueryTopNavMenu as jest.Mock).mock + .calls[0][0]; act(() => - instance.find(services.navigation.ui.AggregateQueryTopNavMenu).prop('onQuerySubmit')!({ + onQuerySubmit({ dateRange: { from: 'now-7d', to: 'now' }, }) ); - - instance.update(); expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/setState', payload: { @@ -1464,15 +1278,12 @@ describe('Lens App', () => { it('updates the state if session id changes from the outside', async () => { const sessionIdS = new Subject<string>(); - const services = makeDefaultServices(sessionIdS, 'sessionId-1'); - const { lensStore } = await mountWith({ props: undefined, services }); + services = makeDefaultServices(sessionIdS, 'sessionId-1'); + const { lensStore } = await renderApp(); - act(() => { - sessionIdS.next('new-session-id'); - }); - await act(async () => { - await new Promise((r) => setTimeout(r, 0)); - }); + act(() => sessionIdS.next('new-session-id')); + + await waitToLoad(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `new-session-id`, @@ -1481,7 +1292,7 @@ describe('Lens App', () => { }); it('does not update the searchSessionId when the state changes', async () => { - const { lensStore } = await mountWith({ preloadedState: { isSaveable: true } }); + const { lensStore } = await renderApp({ preloadedState: { isSaveable: true } }); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: `sessionId-1`, @@ -1491,40 +1302,37 @@ describe('Lens App', () => { }); describe('showing a confirm message when leaving', () => { - let defaultLeave: jest.Mock; - let confirmLeave: jest.Mock; + const defaultLeave = jest.fn(); + const confirmLeave = jest.fn(); beforeEach(() => { - defaultLeave = jest.fn(); - confirmLeave = jest.fn(); + jest.clearAllMocks(); }); 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]; + await renderApp(); + const lastCall = (props.onAppLeave as jest.Mock).mock.lastCall![0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); it('does not confirm if the user is missing save permissions', async () => { - const services = makeDefaultServicesForApp(); - services.application = { - ...services.application, - capabilities: { - ...services.application.capabilities, - visualize: { save: false, saveQuery: false, show: true }, - }, + services.application.capabilities = { + ...services.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, }; - const { props } = await mountWith({ services, preloadedState: { isSaveable: true } }); - const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; + await renderApp({ + preloadedState: { isSaveable: true }, + }); + const lastCall = (props.onAppLeave as jest.Mock).mock.lastCall![0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); it('should confirm when leaving with an unsaved doc', async () => { - const { props } = await mountWith({ + await renderApp({ preloadedState: { visualization: { activeId: 'testVis', @@ -1533,16 +1341,18 @@ describe('Lens App', () => { isSaveable: true, }, }); - const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; + const lastCall = (props.onAppLeave as jest.Mock).mock.calls[ + (props.onAppLeave as jest.Mock).mock.calls.length - 1 + ][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); it('should confirm when leaving with unsaved changes to an existing doc', async () => { - const { props } = await mountWith({ + await renderApp({ preloadedState: { - persistedDoc: defaultDoc, + persistedDoc: getLensDocumentMock(), visualization: { activeId: 'testVis', state: {}, @@ -1550,73 +1360,45 @@ describe('Lens App', () => { isSaveable: true, }, }); - const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; + const lastCall = (props.onAppLeave as jest.Mock).mock.lastCall![0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); it('should confirm when leaving from a context initial doc with changes made in lens', async () => { - const initialProps = { - ...makeDefaultProps(), - contextOriginatingApp: 'TSVB', - initialContext: { - layers: [ - { - indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - xFieldName: 'order_date', - xMode: 'date_histogram', - chartType: 'area', - axisPosition: 'left', - palette: { - type: 'palette', - name: 'default', - }, - metrics: [ - { - agg: 'count', - isFullReference: false, - fieldName: 'document', - params: {}, - color: '#68BC00', - }, - ], - timeInterval: 'auto', - }, - ], - type: 'lnsXY', - configuration: { - fill: 0.5, - legend: { - isVisible: true, - position: 'right', - shouldTruncate: true, - maxLines: 1, - }, - gridLinesVisibility: { - x: true, - yLeft: true, - yRight: true, + props.contextOriginatingApp = 'TSVB'; + props.initialContext = { + layers: [ + { + indexPatternId: 'indexPatternId', + chartType: 'area', + axisPosition: 'left', + palette: { + type: 'palette', + name: 'default', }, - extents: { - yLeftExtent: { - mode: 'full', - }, - yRightExtent: { - mode: 'full', + metrics: [ + { + agg: 'count', + isFullReference: false, + fieldName: 'document', + params: {}, + color: '#68BC00', }, - }, + ], + timeInterval: 'auto', }, - savedObjectId: '', - vizEditorOriginatingAppUrl: '#/tsvb-link', - isVisualizeAction: true, - }, - }; + ], + type: 'lnsXY', + savedObjectId: '', + vizEditorOriginatingAppUrl: '#/tsvb-link', + isVisualizeAction: true, + } as unknown as VisualizeEditorContext; - const mountedApp = await mountWith({ - props: initialProps as unknown as jest.Mocked<LensAppProps>, + await renderApp({ preloadedState: { - persistedDoc: defaultDoc, + persistedDoc: getLensDocumentMock(), visualization: { activeId: 'testVis', state: {}, @@ -1624,76 +1406,72 @@ describe('Lens App', () => { isSaveable: true, }, }); - const lastCall = - mountedApp.props.onAppLeave.mock.calls[ - mountedApp.props.onAppLeave.mock.calls.length - 1 - ][0]; + const lastCall = (props.onAppLeave as jest.Mock).mock.lastCall![0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).not.toHaveBeenCalled(); expect(confirmLeave).toHaveBeenCalled(); }); it('should not confirm when changes are saved', async () => { + const localDoc = getLensDocumentMock(); const preloadedState = { persistedDoc: { - ...defaultDoc, + ...localDoc, state: { - ...defaultDoc.state, - datasourceStates: { testDatasource: {} }, + ...localDoc.state, + datasourceStates: { + testDatasource: 'datasource', + }, visualization: {}, }, }, isSaveable: true, - ...(defaultDoc.state as Partial<LensAppState>), + ...(localDoc.state as Partial<LensAppState>), visualization: { activeId: 'testVis', state: {}, }, }; - const customProps = makeDefaultProps(); - customProps.datasourceMap.testDatasource.isEqual = () => true; // if this returns false, the documents won't be accounted equal + props.datasourceMap.testDatasource.isEqual = jest.fn().mockReturnValue(true); // if this returns false, the documents won't be accounted equal - const { props } = await mountWith({ preloadedState, props: customProps }); + await renderApp({ preloadedState }); - const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; - lastCall({ default: defaultLeave, confirm: confirmLeave }); + const lastCallArg = props.onAppLeave.mock.lastCall![0]; + lastCallArg?.({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); - // not sure how to test it it('should confirm when the latest doc is invalid', async () => { - const { lensStore, props } = await mountWith({}); - act(() => { - lensStore.dispatch( + const { lensStore } = await renderApp(); + await act(async () => { + await lensStore.dispatch( setState({ - persistedDoc: defaultDoc, + persistedDoc: getLensDocumentMock(), isSaveable: true, }) ); }); - const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; + const lastCall = (props.onAppLeave as jest.Mock).mock.lastCall![0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); }); + it('should display a conflict callout if saved object conflicts', async () => { const history = createMemoryHistory(); - const { services } = await mountWith({ - props: { - ...makeDefaultProps(), - history: { - ...history, - location: { - ...history.location, - search: '?_g=test', - }, - }, + props.history = { + ...history, + location: { + ...history.location, + search: '?_g=test', }, + }; + await renderApp({ preloadedState: { - persistedDoc: defaultDoc, + persistedDoc: getLensDocumentMock({ savedObjectId: defaultSavedObjectId }), sharingSavedObjectProps: { outcome: 'conflict', aliasTargetId: '2', @@ -1701,7 +1479,7 @@ describe('Lens App', () => { }, }); expect(services.spaces?.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ - currentObjectId: '1234', + currentObjectId: defaultSavedObjectId, objectNoun: 'Lens visualization', otherObjectId: '2', otherObjectPath: '#/edit/2?_g=test', diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 60e1b0dfdb668..b8903bde1af0f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -9,16 +9,14 @@ import './app.scss'; import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import type { TimeRange } from '@kbn/es-query'; -import { EuiBreadcrumb, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public'; import { OnSaveProps } from '@kbn/saved-objects-plugin/public'; import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; -import type { LensAppLocatorParams } from '../../common/locator/locator'; import { LensAppProps, LensAppServices } from './types'; import { LensTopNavMenu } from './lens_top_nav'; -import { LensByReferenceInput } from '../embeddable'; -import { AddUserMessages, EditorFrameInstance, UserMessagesGetter } from '../types'; -import { Document } from '../persistence/saved_object_store'; +import { AddUserMessages, EditorFrameInstance, Simplify, UserMessagesGetter } from '../types'; +import { LensDocument } from '../persistence/saved_object_store'; import { setState, @@ -43,15 +41,24 @@ import { import { replaceIndexpattern } from '../state_management/lens_slice'; import { useApplicationUserMessages } from './get_application_user_messages'; import { trackSaveUiCounterEvents } from '../lens_ui_telemetry'; - -export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & { - returnToOrigin: boolean; - dashboardId?: string | null; - onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; - newDescription?: string; - newTags?: string[]; - panelTimeRange?: TimeRange; -}; +import { + getCurrentTitle, + isLegacyEditorEmbeddable, + setBreadcrumbsTitle, + useNavigateBackToApp, + useShortUrlService, +} from './app_helpers'; + +export type SaveProps = Simplify< + Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & { + returnToOrigin: boolean; + dashboardId?: string | null; + onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; + newDescription?: string; + newTags?: string[]; + panelTimeRange?: TimeRange; + } +>; export function App({ history, @@ -127,18 +134,26 @@ export function App({ selectSavedObjectFormat(state, selectorDependencies) ); - const shortUrls = useMemo(() => share?.url.shortUrls.get(null), [share]); - // 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, setLastKnownDoc] = useState<Document | undefined>(undefined); - const [initialDocFromContext, setInitialDocFromContext] = useState<Document | undefined>( + const [lastKnownDoc, setLastKnownDoc] = useState<LensDocument | undefined>(undefined); + const [initialDocFromContext, setInitialDocFromContext] = useState<LensDocument | undefined>( undefined ); - const [isGoBackToVizEditorModalVisible, setIsGoBackToVizEditorModalVisible] = useState(false); const [shouldCloseAndSaveTextBasedQuery, setShouldCloseAndSaveTextBasedQuery] = useState(false); - const savedObjectId = (initialInput as LensByReferenceInput)?.savedObjectId; + const savedObjectId = initialInput?.savedObjectId; + + const isFromLegacyEditorEmbeddable = isLegacyEditorEmbeddable(initialContext); + const legacyEditorAppName = + initialContext && 'originatingApp' in initialContext + ? initialContext.originatingApp + : undefined; + const legacyEditorAppUrl = + initialContext && 'vizEditorOriginatingAppUrl' in initialContext + ? initialContext.vizEditorOriginatingAppUrl + : undefined; + const initialContextIsEmbedded = Boolean(legacyEditorAppName); useEffect(() => { if (currentDoc) { @@ -167,18 +182,27 @@ export function App({ [isLinkedToOriginatingApp, savedObjectId] ); + // Wrap the isEqual call to avoid to carry all the static references + // around all the time. + const isLensEqualWrapper = useCallback( + (refDoc: LensDocument | undefined) => { + return isLensEqual( + refDoc, + lastKnownDoc, + data.query.filterManager.inject.bind(data.query.filterManager), + datasourceMap, + visualizationMap, + annotationGroups + ); + }, + [annotationGroups, data.query.filterManager, datasourceMap, lastKnownDoc, visualizationMap] + ); + useEffect(() => { onAppLeave((actions) => { if ( application.capabilities.visualize.save && - !isLensEqual( - persistedDoc, - lastKnownDoc, - data.query.filterManager.inject.bind(data.query.filterManager), - datasourceMap, - visualizationMap, - annotationGroups - ) && + !isLensEqualWrapper(persistedDoc) && (isSaveable || persistedDoc) ) { return actions.confirm( @@ -208,6 +232,7 @@ export function App({ datasourceMap, visualizationMap, annotationGroups, + isLensEqualWrapper, ]); const getLegacyUrlConflictCallout = useCallback(() => { @@ -235,66 +260,17 @@ export function App({ // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { const isByValueMode = getIsByValueMode(); - const comesFromVizEditorDashboard = - initialContext && 'originatingApp' in initialContext && initialContext.originatingApp; - const breadcrumbs: EuiBreadcrumb[] = []; - if ( - (isLinkedToOriginatingApp || comesFromVizEditorDashboard) && - getOriginatingAppName() && - redirectToOrigin - ) { - breadcrumbs.push({ - onClick: () => { - redirectToOrigin(); - }, - text: getOriginatingAppName(), - }); - } - if (!isByValueMode) { - breadcrumbs.push({ - href: application.getUrlForApp('visualize'), - onClick: (e) => { - application.navigateToApp('visualize', { path: '/' }); - e.preventDefault(); - }, - text: i18n.translate('xpack.lens.breadcrumbsTitle', { - defaultMessage: 'Visualize Library', - }), - }); - } - let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', { - defaultMessage: 'Create', - }); - if (persistedDoc) { - currentDocTitle = isByValueMode - ? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' }) - : persistedDoc.title; - } - if ( - !persistedDoc?.title && - initialContext && - 'isEmbeddable' in initialContext && - initialContext.isEmbeddable - ) { - currentDocTitle = i18n.translate('xpack.lens.breadcrumbsEditInLensFromDashboard', { - defaultMessage: 'Converting {title} visualization', - values: { - title: initialContext.title ? `"${initialContext.title}"` : initialContext.visTypeTitle, - }, - }); - } - - const currentDocBreadcrumb: EuiBreadcrumb = { text: currentDocTitle }; - breadcrumbs.push(currentDocBreadcrumb); - if (serverless?.setBreadcrumbs) { - // TODO: https://github.com/elastic/kibana/issues/163488 - // for now, serverless breadcrumbs only set the title, - // the rest of the breadcrumbs are handled by the serverless navigation - // the serverless navigation is not yet aware of the byValue/originatingApp context - serverless.setBreadcrumbs(currentDocBreadcrumb); - } else { - chrome.setBreadcrumbs(breadcrumbs); - } + const currentDocTitle = getCurrentTitle(persistedDoc, isByValueMode, initialContext); + setBreadcrumbsTitle( + { application, chrome, serverless }, + { + isByValueMode, + currentDocTitle, + redirectToOrigin, + isFromLegacyEditor: Boolean(isLinkedToOriginatingApp || legacyEditorAppName), + originatingAppName: getOriginatingAppName(), + } + ); }, [ getOriginatingAppName, redirectToOrigin, @@ -303,8 +279,10 @@ export function App({ chrome, isLinkedToOriginatingApp, persistedDoc, - initialContext, + isFromLegacyEditorEmbeddable, + legacyEditorAppName, serverless, + initialContext, ]); const switchDatasource = useCallback(() => { @@ -314,12 +292,13 @@ export function App({ }, []); const runSave = useCallback( - (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { + async (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { dispatch(applyChanges()); const prevVisState = persistedDoc?.visualizationType === visualization.activeId ? persistedDoc?.state.visualization : undefined; + const telemetryEvents = activeVisualization?.getTelemetryEventsOnSave?.( visualization.state, prevVisState @@ -327,36 +306,33 @@ export function App({ if (telemetryEvents && telemetryEvents.length) { trackSaveUiCounterEvents(telemetryEvents); } - return runSaveLensVisualization( - { - lastKnownDoc, - getIsByValueMode, - savedObjectsTagging, - initialInput, - redirectToOrigin, - persistedDoc, - onAppLeave, - redirectTo, - switchDatasource, - originatingApp: incomingState?.originatingApp, - textBasedLanguageSave: shouldCloseAndSaveTextBasedQuery, - ...lensAppServices, - }, - saveProps, - options - ).then( - (newState) => { - if (newState) { - dispatchSetState(newState); - setIsSaveModalVisible(false); - setShouldCloseAndSaveTextBasedQuery(false); - } - }, - () => { - // error is handled inside the modal - // so ignoring it here + try { + const newState = await runSaveLensVisualization( + { + lastKnownDoc, + savedObjectsTagging, + initialInput, + redirectToOrigin, + persistedDoc, + onAppLeave, + redirectTo, + switchDatasource, + originatingApp: incomingState?.originatingApp, + textBasedLanguageSave: shouldCloseAndSaveTextBasedQuery, + ...lensAppServices, + }, + saveProps, + options + ); + if (newState) { + dispatchSetState(newState); + setIsSaveModalVisible(false); + setShouldCloseAndSaveTextBasedQuery(false); } - ); + } catch (e) { + // error is handled inside the modal + // so ignoring it here + } }, [ visualization.activeId, @@ -364,7 +340,6 @@ export function App({ activeVisualization, dispatch, lastKnownDoc, - getIsByValueMode, savedObjectsTagging, initialInput, redirectToOrigin, @@ -386,67 +361,20 @@ export function App({ } }, [lastKnownDoc, initialDocFromContext]); - // if users comes to Lens from the Viz editor, they should have the option to navigate back - const goBackToOriginatingApp = useCallback(() => { - if ( - initialContext && - 'vizEditorOriginatingAppUrl' in initialContext && - initialContext.vizEditorOriginatingAppUrl - ) { - const [initialDocFromContextUnchanged, currentDocHasBeenSavedInLens] = [ - initialDocFromContext, - persistedDoc, - ].map((refDoc) => - isLensEqual( - refDoc, - lastKnownDoc, - data.query.filterManager.inject, - datasourceMap, - visualizationMap, - annotationGroups - ) - ); - if (initialDocFromContextUnchanged || currentDocHasBeenSavedInLens) { - onAppLeave((actions) => { - return actions.default(); - }); - application.navigateToApp('visualize', { path: initialContext.vizEditorOriginatingAppUrl }); - } else { - setIsGoBackToVizEditorModalVisible(true); - } - } - }, [ - annotationGroups, + const { + shouldShowGoBackToVizEditorModal, + goBackToOriginatingApp, + navigateToVizEditor, + closeGoBackToVizEditorModal, + } = useNavigateBackToApp({ application, - data.query.filterManager.inject, - datasourceMap, - initialContext, - initialDocFromContext, - lastKnownDoc, onAppLeave, + legacyEditorAppName, + legacyEditorAppUrl, + initialDocFromContext, persistedDoc, - visualizationMap, - ]); - - const navigateToVizEditor = useCallback(() => { - setIsGoBackToVizEditorModalVisible(false); - if ( - initialContext && - 'vizEditorOriginatingAppUrl' in initialContext && - initialContext.vizEditorOriginatingAppUrl - ) { - onAppLeave((actions) => { - return actions.default(); - }); - application.navigateToApp('visualize', { path: initialContext.vizEditorOriginatingAppUrl }); - } - }, [application, initialContext, onAppLeave]); - - const initialContextIsEmbedded = useMemo(() => { - return Boolean( - initialContext && 'originatingApp' in initialContext && initialContext.originatingApp - ); - }, [initialContext]); + isLensEqual: isLensEqualWrapper, + }); const indexPatternService = useMemo( () => @@ -471,35 +399,12 @@ export function App({ [dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch] ); - // remember latest URL based on the configuration - // url_panel_content has a similar logic - const shareURLCache = useRef({ params: '', url: '' }); - - const shortUrlService = useCallback( - async (params: LensAppLocatorParams) => { - const cacheKey = JSON.stringify(params); - if (shareURLCache.current.params === cacheKey) { - return shareURLCache.current.url; - } - if (locator && shortUrls) { - // This is a stripped down version of what the share URL plugin is doing - const shortUrl = await shortUrls.createWithLocator({ locator, params }); - const absoluteShortUrl = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true }); - shareURLCache.current = { params: cacheKey, url: absoluteShortUrl }; - return absoluteShortUrl; - } - return ''; - }, - [locator, shortUrls] - ); + const shortUrlService = useShortUrlService(locator, share); const isManaged = useLensSelector(selectIsManaged); const returnToOriginSwitchLabelForContext = - initialContext && - 'isEmbeddable' in initialContext && - initialContext.isEmbeddable && - !persistedDoc + isFromLegacyEditorEmbeddable && !persistedDoc ? i18n.translate('xpack.lens.app.replacePanel', { defaultMessage: 'Replace panel on {originatingApp}', values: { @@ -547,16 +452,7 @@ export function App({ title={persistedDoc?.title} lensInspector={lensInspector} currentDoc={currentDoc} - isCurrentStateDirty={ - !isLensEqual( - persistedDoc, - lastKnownDoc, - data.query.filterManager.inject.bind(data.query.filterManager), - datasourceMap, - visualizationMap, - annotationGroups - ) - } + isCurrentStateDirty={!isLensEqualWrapper(persistedDoc)} goBackToOriginatingApp={goBackToOriginatingApp} contextOriginatingApp={contextOriginatingApp} initialContextIsEmbedded={initialContextIsEmbedded} @@ -612,13 +508,13 @@ export function App({ } /> )} - {isGoBackToVizEditorModalVisible && ( + {shouldShowGoBackToVizEditorModal && ( <EuiConfirmModal maxWidth={600} title={i18n.translate('xpack.lens.app.unsavedWorkTitle', { defaultMessage: 'Unsaved changes', })} - onCancel={() => setIsGoBackToVizEditorModalVisible(false)} + onCancel={closeGoBackToVizEditorModal} onConfirm={navigateToVizEditor} cancelButtonText={i18n.translate('xpack.lens.app.goBackModalCancelBtn', { defaultMessage: 'Cancel', diff --git a/x-pack/plugins/lens/public/app_plugin/app_helpers.test.ts b/x-pack/plugins/lens/public/app_plugin/app_helpers.test.ts new file mode 100644 index 0000000000000..7dc4e8cfda78c --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/app_helpers.test.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 { renderHook, act } from '@testing-library/react-hooks'; +import faker from 'faker'; +import { UseNavigateBackToAppProps, useNavigateBackToApp } from './app_helpers'; +import { defaultDoc, makeDefaultServices } from '../mocks/services_mock'; +import { cloneDeep } from 'lodash'; +import { LensDocument } from '../persistence'; + +function getLensDocumentMock(someProps?: Partial<LensDocument>) { + return cloneDeep({ ...defaultDoc, ...someProps }); +} + +const getApplicationMock = () => makeDefaultServices().application; + +describe('App helpers', () => { + function getDefaultProps( + someProps?: Partial<UseNavigateBackToAppProps> + ): UseNavigateBackToAppProps { + return { + application: getApplicationMock(), + onAppLeave: jest.fn(), + legacyEditorAppName: faker.lorem.word(), + legacyEditorAppUrl: faker.internet.url(), + isLensEqual: jest.fn(() => true), + initialDocFromContext: undefined, + persistedDoc: getLensDocumentMock(), + ...someProps, + }; + } + describe('useNavigateBackToApp', () => { + it('navigates back to originating app if documents has not changed', () => { + const props = getDefaultProps(); + const { result } = renderHook(() => useNavigateBackToApp(props)); + + act(() => { + result.current.goBackToOriginatingApp(); + }); + + expect(props.application.navigateToApp).toHaveBeenCalledWith(props.legacyEditorAppName, { + path: props.legacyEditorAppUrl, + }); + }); + + it('shows modal if documents are not equal', () => { + const props = getDefaultProps({ isLensEqual: jest.fn().mockReturnValue(false) }); + const { result } = renderHook(() => useNavigateBackToApp(props)); + + act(() => { + result.current.goBackToOriginatingApp(); + }); + + expect(props.application.navigateToApp).not.toHaveBeenCalled(); + expect(result.current.shouldShowGoBackToVizEditorModal).toBe(true); + }); + + it('navigateToVizEditor hides modal and navigates back to Viz editor', () => { + const props = getDefaultProps(); + const { result } = renderHook(() => useNavigateBackToApp(props)); + + act(() => { + result.current.navigateToVizEditor(); + }); + + expect(result.current.shouldShowGoBackToVizEditorModal).toBe(false); + expect(props.application.navigateToApp).toHaveBeenCalledWith(props.legacyEditorAppName, { + path: props.legacyEditorAppUrl, + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/app_helpers.ts b/x-pack/plugins/lens/public/app_plugin/app_helpers.ts new file mode 100644 index 0000000000000..4e240ac17159a --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/app_helpers.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { AppLeaveHandler, ApplicationStart } from '@kbn/core-application-browser'; +import { ChromeStart } from '@kbn/core-chrome-browser'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; +import { useRef, useCallback, useMemo, useState } from 'react'; +import { SharePublicStart } from '@kbn/share-plugin/public/plugin'; +import { LensAppLocator, LensAppLocatorParams } from '../../common/locator/locator'; +import { VisualizeEditorContext } from '../types'; +import { LensDocument } from '../persistence'; +import { RedirectToOriginProps } from './types'; + +const VISUALIZE_APP_ID = 'visualize'; + +export function isLegacyEditorEmbeddable( + initialContext: VisualizeEditorContext | VisualizeFieldContext | undefined +): initialContext is VisualizeEditorContext { + return Boolean(initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable); +} + +export function getCurrentTitle( + persistedDoc: LensDocument | undefined, + isByValueMode: boolean, + initialContext: VisualizeEditorContext | VisualizeFieldContext | undefined +) { + if (persistedDoc) { + if (isByValueMode) { + return i18n.translate('xpack.lens.breadcrumbsByValue', { + defaultMessage: 'Edit visualization', + }); + } + if (persistedDoc.title) { + return persistedDoc.title; + } + } + if (!persistedDoc?.title && isLegacyEditorEmbeddable(initialContext)) { + return i18n.translate('xpack.lens.breadcrumbsEditInLensFromDashboard', { + defaultMessage: 'Converting {title} visualization', + values: { + title: initialContext.title ? `"${initialContext.title}"` : initialContext.visTypeTitle, + }, + }); + } + return i18n.translate('xpack.lens.breadcrumbsCreate', { + defaultMessage: 'Create', + }); +} + +export function setBreadcrumbsTitle( + { + application, + serverless, + chrome, + }: { + application: ApplicationStart; + serverless: ServerlessPluginStart | undefined; + chrome: ChromeStart; + }, + { + isByValueMode, + originatingAppName, + redirectToOrigin, + isFromLegacyEditor, + currentDocTitle, + }: { + isByValueMode: boolean; + originatingAppName: string | undefined; + redirectToOrigin: ((props?: RedirectToOriginProps | undefined) => void) | undefined; + isFromLegacyEditor: boolean; + currentDocTitle: string; + } +) { + const breadcrumbs: EuiBreadcrumb[] = []; + if (isFromLegacyEditor && originatingAppName && redirectToOrigin) { + breadcrumbs.push({ + onClick: () => { + redirectToOrigin(); + }, + text: originatingAppName, + }); + } + if (!isByValueMode) { + breadcrumbs.push({ + href: application.getUrlForApp(VISUALIZE_APP_ID), + onClick: (e) => { + application.navigateToApp(VISUALIZE_APP_ID, { path: '/' }); + e.preventDefault(); + }, + text: i18n.translate('xpack.lens.breadcrumbsTitle', { + defaultMessage: 'Visualize Library', + }), + }); + } + + const currentDocBreadcrumb: EuiBreadcrumb = { text: currentDocTitle }; + breadcrumbs.push(currentDocBreadcrumb); + if (serverless?.setBreadcrumbs) { + // TODO: https://github.com/elastic/kibana/issues/163488 + // for now, serverless breadcrumbs only set the title, + // the rest of the breadcrumbs are handled by the serverless navigation + // the serverless navigation is not yet aware of the byValue/originatingApp context + serverless.setBreadcrumbs(currentDocBreadcrumb); + } else { + chrome.setBreadcrumbs(breadcrumbs); + } +} + +export function useShortUrlService( + locator: LensAppLocator | undefined, + share: SharePublicStart | undefined +) { + const shortUrls = useMemo(() => share?.url.shortUrls.get(null), [share]); + // remember latest URL based on the configuration + // url_panel_content has a similar logic + const shareURLCache = useRef({ params: '', url: '' }); + + return useCallback( + async (params: LensAppLocatorParams) => { + const cacheKey = JSON.stringify(params); + if (shareURLCache.current.params === cacheKey) { + return shareURLCache.current.url; + } + if (locator && shortUrls) { + // This is a stripped down version of what the share URL plugin is doing + const shortUrl = await shortUrls.createWithLocator({ locator, params }); + const absoluteShortUrl = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true }); + shareURLCache.current = { params: cacheKey, url: absoluteShortUrl }; + return absoluteShortUrl; + } + return ''; + }, + [locator, shortUrls] + ); +} + +export interface UseNavigateBackToAppProps { + application: ApplicationStart; + onAppLeave: (handler: AppLeaveHandler) => void; + legacyEditorAppName: string | undefined; + legacyEditorAppUrl: string | undefined; + initialDocFromContext: LensDocument | undefined; + persistedDoc: LensDocument | undefined; + isLensEqual: (refDoc: LensDocument | undefined) => boolean; +} + +export function useNavigateBackToApp({ + application, + onAppLeave, + legacyEditorAppName, + legacyEditorAppUrl, + initialDocFromContext, + persistedDoc, + isLensEqual, +}: UseNavigateBackToAppProps) { + const [shouldShowGoBackToVizEditorModal, setIsGoBackToVizEditorModalVisible] = useState(false); + /** Shared logic to navigate back to the originating viz editor app */ + const navigateBackToVizEditor = useCallback(() => { + if (legacyEditorAppUrl) { + onAppLeave((actions) => { + return actions.default(); + }); + application.navigateToApp(legacyEditorAppName || VISUALIZE_APP_ID, { + path: legacyEditorAppUrl, + }); + } + }, [application, legacyEditorAppName, legacyEditorAppUrl, onAppLeave]); + + // if users comes to Lens from the Viz editor, they should have the option to navigate back + // used for TopNavMenu + const goBackToOriginatingApp = useCallback(() => { + if (legacyEditorAppUrl) { + if ([initialDocFromContext, persistedDoc].some(isLensEqual)) { + navigateBackToVizEditor(); + } else { + setIsGoBackToVizEditorModalVisible(true); + } + } + }, [ + legacyEditorAppUrl, + initialDocFromContext, + persistedDoc, + isLensEqual, + navigateBackToVizEditor, + setIsGoBackToVizEditorModalVisible, + ]); + + // Used for Saving Modal + const navigateToVizEditor = useCallback(() => { + setIsGoBackToVizEditorModalVisible(false); + navigateBackToVizEditor(); + }, [navigateBackToVizEditor, setIsGoBackToVizEditorModalVisible]); + + return { + shouldShowGoBackToVizEditorModal, + goBackToOriginatingApp, + navigateToVizEditor, + closeGoBackToVizEditorModal: () => setIsGoBackToVizEditorModalVisible(false), + }; +} diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx index 1afa1974de351..a470cf41cf837 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx @@ -15,9 +15,12 @@ import { UserMessageGetterProps, filterAndSortUserMessages, getApplicationUserMessages, + handleMessageOverwriteFromConsumer, } from './get_application_user_messages'; import { cleanup, render, screen } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; +import { FIELD_NOT_FOUND, FIELD_WRONG_TYPE } from '../user_messages_ids'; +import { LensPublicCallbacks } from '../react_embeddable/types'; import { getLongMessage } from '../user_messages_utils'; jest.mock('@kbn/shared-ux-link-redirect-app', () => { @@ -388,4 +391,100 @@ describe('filtering user messages', () => { ] `); }); + + describe('override messages with custom callback', () => { + it('should override embeddableBadge message', async () => { + const getBadgeMessage = jest.fn( + (): ReturnType<NonNullable<LensPublicCallbacks['onBeforeBadgesRender']>> => [ + { + uniqueId: FIELD_NOT_FOUND, + severity: 'warning', + fixableInEditor: true, + displayLocations: [ + { id: 'embeddableBadge' }, + { id: 'dimensionButton', dimensionId: '1' }, + ], + longMessage: 'custom', + shortMessage: '', + hidePopoverIcon: true, + }, + ] + ); + + expect( + handleMessageOverwriteFromConsumer( + [ + { + uniqueId: FIELD_NOT_FOUND, + severity: 'error', + fixableInEditor: true, + displayLocations: [ + { id: 'embeddableBadge' }, + { id: 'dimensionButton', dimensionId: '1' }, + ], + longMessage: 'original', + shortMessage: '', + }, + { + uniqueId: FIELD_WRONG_TYPE, + severity: 'error', + fixableInEditor: true, + displayLocations: [{ id: 'visualization' }], + longMessage: 'original', + shortMessage: '', + }, + ], + getBadgeMessage + ) + ).toEqual( + expect.arrayContaining([ + { + uniqueId: FIELD_WRONG_TYPE, + severity: 'error', + fixableInEditor: true, + displayLocations: [{ id: 'visualization' }], + longMessage: 'original', + shortMessage: '', + }, + { + uniqueId: FIELD_NOT_FOUND, + severity: 'warning', + fixableInEditor: true, + displayLocations: [ + { id: 'embeddableBadge' }, + { id: 'dimensionButton', dimensionId: '1' }, + ], + longMessage: 'custom', + shortMessage: '', + hidePopoverIcon: true, + }, + ]) + ); + }); + + it('should not override embeddableBadge message if callback is not provided', async () => { + const messages: UserMessage[] = [ + { + uniqueId: FIELD_NOT_FOUND, + severity: 'error', + fixableInEditor: true, + displayLocations: [ + { id: 'embeddableBadge' }, + { id: 'dimensionButton', dimensionId: '1' }, + ], + longMessage: 'original', + shortMessage: '', + }, + { + uniqueId: FIELD_WRONG_TYPE, + severity: 'error', + fixableInEditor: true, + displayLocations: [{ id: 'visualization' }], + longMessage: 'original', + shortMessage: '', + }, + ]; + expect(handleMessageOverwriteFromConsumer(messages)).toEqual(messages); + }); + }); }); diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx index d7d04a837e08a..b2755a411e719 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx @@ -11,6 +11,7 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CoreStart } from '@kbn/core/public'; import { Dispatch } from '@reduxjs/toolkit'; +import { partition } from 'lodash'; import { updateDatasourceState, type DataViewsState, @@ -35,6 +36,8 @@ import { EDITOR_UNKNOWN_DATASOURCE_TYPE, EDITOR_UNKNOWN_VIS_TYPE, } from '../user_messages_ids'; +import { nonNullable } from '../utils'; +import type { LensPublicCallbacks } from '../react_embeddable/types'; export interface UserMessageGetterProps { visualizationType: string | null | undefined; @@ -203,21 +206,38 @@ function getMissingIndexPatternsErrors( ]; } +export const handleMessageOverwriteFromConsumer = ( + messages: UserMessage[], + onBeforeBadgesRender?: LensPublicCallbacks['onBeforeBadgesRender'] +) => { + if (onBeforeBadgesRender) { + // we need something else to better identify those errors + const [messagesToHandle, originalMessages] = partition(messages, (message) => + message.displayLocations.some((location) => location.id === 'embeddableBadge') + ); + + if (messagesToHandle.length > 0) { + const customBadgeMessages = onBeforeBadgesRender(messagesToHandle); + return originalMessages.concat(customBadgeMessages); + } + } + + return messages; +}; + export const filterAndSortUserMessages = ( userMessages: UserMessage[], locationId?: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[], { dimensionId, severity }: UserMessageFilters = {} ) => { - const locationIds = Array.isArray(locationId) - ? locationId - : typeof locationId === 'string' - ? [locationId] - : []; + const locationIds = new Set( + (Array.isArray(locationId) ? locationId : [locationId]).filter(nonNullable) + ); const filteredMessages = userMessages.filter((message) => { - if (locationIds.length) { + if (locationIds.size) { const hasMatch = message.displayLocations.some((location) => { - if (!locationIds.includes(location.id)) { + if (!locationIds.has(location.id)) { return false; } @@ -229,11 +249,7 @@ export const filterAndSortUserMessages = ( } } - if (severity && message.severity !== severity) { - return false; - } - - return true; + return !severity || message.severity === severity; }); return filteredMessages.sort(bySeverity); @@ -329,7 +345,7 @@ export const useApplicationUserMessages = ({ const getUserMessages: UserMessagesGetter = (locationId, filterArgs) => filterAndSortUserMessages( - [...userMessages, ...Object.values(additionalUserMessages)], + userMessages.concat(Object.values(additionalUserMessages)), locationId, filterArgs ?? {} ); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts index babde51e39f27..8371a77793ea3 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts @@ -7,7 +7,7 @@ import { Filter, FilterStateStore } from '@kbn/es-query'; import { isLensEqual } from './lens_document_equality'; -import { Document } from '../persistence/saved_object_store'; +import { LensDocument } from '../persistence/saved_object_store'; import { AnnotationGroups, Datasource, @@ -18,7 +18,7 @@ import { const visualizationType = 'lnsSomeVis'; -const defaultDoc: Document = { +const defaultDoc: LensDocument = { title: 'some-title', visualizationType, state: { @@ -105,7 +105,7 @@ describe('lens document equality', () => { expect( isLensEqual( undefined, - {} as Document, + {} as LensDocument, mockInjectFilterReferences, {}, mockVisualizationMap, @@ -114,7 +114,7 @@ describe('lens document equality', () => { ).toBeFalsy(); expect( isLensEqual( - {} as Document, + {} as LensDocument, undefined, mockInjectFilterReferences, {}, diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts index 60316802ca5ea..4fc97882fd926 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts @@ -7,7 +7,7 @@ import { isEqual, intersection, union } from 'lodash'; import { FilterManager } from '@kbn/data-plugin/public'; -import { Document } from '../persistence/saved_object_store'; +import { LensDocument } from '../persistence/saved_object_store'; import { AnnotationGroups, DatasourceMap, VisualizationMap } from '../types'; import { removePinnedFilters } from './save_modal_container'; @@ -15,8 +15,8 @@ const removeNonSerializable = (obj: Parameters<JSON['stringify']>[0]) => JSON.parse(JSON.stringify(obj)); export const isLensEqual = ( - doc1In: Document | undefined, - doc2In: Document | undefined, + doc1In: LensDocument | undefined, + doc2In: LensDocument | undefined, injectFilterReferences: FilterManager['inject'], datasourceMap: DatasourceMap, visualizationMap: VisualizationMap, @@ -54,6 +54,7 @@ export const isLensEqual = ( } })() : isEqual(doc1.state.visualization, doc2.state.visualization); + if (!visualizationStateIsEqual) { return false; } @@ -68,16 +69,14 @@ export const isLensEqual = ( if (datasourcesEqual) { // equal so far, so actually check - datasourcesEqual = availableDatasourceTypes1 - .map((type) => - datasourceMap[type].isEqual( - doc1.state.datasourceStates[type], - [...doc1.references, ...(doc1.state.internalReferences || [])], - doc2.state.datasourceStates[type], - [...doc2.references, ...(doc2.state.internalReferences || [])] - ) + datasourcesEqual = availableDatasourceTypes1.every((type) => + datasourceMap[type].isEqual( + doc1.state.datasourceStates[type], + doc1.references.concat(doc1.state.internalReferences || []), + doc2.state.datasourceStates[type], + doc2.references.concat(doc2.state.internalReferences || []) ) - .every((res) => res); + ); } if (!datasourcesEqual) { @@ -96,7 +95,7 @@ export const isLensEqual = ( function injectDocFilterReferences( injectFilterReferences: FilterManager['inject'], - doc?: Document + doc?: LensDocument ) { if (!doc) return undefined; return { 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 399c849b6ebcf..cf76df44eefc0 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 @@ -37,7 +37,6 @@ import { } from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; import { changeIndexPattern } from '../state_management/lens_slice'; -import { LensByReferenceInput } from '../embeddable'; import { DEFAULT_LENS_LAYOUT_DIMENSIONS, getShareURL } from './share_action'; import { getDatasourceLayers } from '../state_management/utils'; @@ -291,7 +290,6 @@ export const LensTopNavMenu = ({ navigation, uiSettings, application, - attributeService, share, dataViewFieldEditor, dataViewEditor, @@ -529,11 +527,9 @@ export const LensTopNavMenu = ({ const topNavConfig = useMemo(() => { const showReplaceInDashboard = - initialContext?.originatingApp === 'dashboards' && - !(initialInput as LensByReferenceInput)?.savedObjectId; + initialContext?.originatingApp === 'dashboards' && !initialInput?.savedObjectId; const showReplaceInCanvas = - initialContext?.originatingApp === 'canvas' && - !(initialInput as LensByReferenceInput)?.savedObjectId; + initialContext?.originatingApp === 'canvas' && !initialInput?.savedObjectId; const contextFromEmbeddable = initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable; @@ -690,8 +686,7 @@ export const LensTopNavMenu = ({ panelTimeRange: contextFromEmbeddable ? initialContext.panelTimeRange : undefined, }, { - saveToLibrary: - (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + saveToLibrary: Boolean(initialInput?.savedObjectId), } ); } @@ -801,7 +796,6 @@ export const LensTopNavMenu = ({ defaultLensTitle, onAppLeave, runSave, - attributeService, setIsSaveModalVisible, goBackToOriginatingApp, redirectToOrigin, diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 7f91943eade30..c431f48f0c403 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -33,12 +33,7 @@ import { EditorFrameStart, LensTopNavMenuEntryGenerator, VisualizeEditorContext import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common/constants'; -import { - LensEmbeddableInput, - LensByReferenceInput, - LensByValueInput, -} from '../embeddable/embeddable'; -import { LensAttributeService } from '../lens_attribute_service'; +import { LensAttributesService } from '../lens_attribute_service'; import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types'; import { makeConfigureStore, @@ -55,6 +50,7 @@ import { MainHistoryLocationState, } from '../../common/locator/locator'; import { SavedObjectIndexStore } from '../persistence'; +import { LensSerializedState } from '../react_embeddable/types'; function getInitialContext(history: AppMountParameters['history']) { const historyLocationState = history.location.state as @@ -83,7 +79,7 @@ function getInitialContext(history: AppMountParameters['history']) { export async function getLensServices( coreStart: CoreStart, startDependencies: LensPluginStartDependencies, - attributeService: LensAttributeService, + attributeService: LensAttributesService, initialContext?: VisualizeFieldContext | VisualizeEditorContext, locator?: LensAppLocator ): Promise<LensAppServices> { @@ -146,7 +142,7 @@ export async function mountApp( params: AppMountParameters, mountProps: { createEditorFrame: EditorFrameStart['createInstance']; - attributeService: LensAttributeService; + attributeService: LensAttributesService; topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; locator?: LensAppLocator; } @@ -188,12 +184,12 @@ export async function mountApp( i18n.translate('xpack.lens.pageTitle', { defaultMessage: 'Lens' }) ); - const getInitialInput = (id?: string, editByValue?: boolean): LensEmbeddableInput | undefined => { + const getInitialInput = (id?: string, editByValue?: boolean): LensSerializedState | undefined => { if (editByValue) { - return embeddableEditorIncomingState?.valueInput as LensByValueInput; + return embeddableEditorIncomingState?.valueInput as LensSerializedState; } if (id) { - return { savedObjectId: id } as LensByReferenceInput; + return { savedObjectId: id } as LensSerializedState; } }; @@ -220,14 +216,14 @@ export async function mountApp( if (initialContext && 'embeddableId' in initialContext) { embeddableId = initialContext.embeddableId; } - if (stateTransfer && props?.input) { - const { input, isCopied } = props; + if (stateTransfer && props?.state) { + const { state, isCopied } = props; stateTransfer.navigateToWithEmbeddablePackage(mergedOriginatingApp, { path: embeddableEditorIncomingState?.originatingPath, state: { embeddableId: isCopied ? undefined : embeddableId, type: LENS_EMBEDDABLE_TYPE, - input, + input: { ...state, savedObject: state.savedObjectId }, searchSessionId: data.search.session.getSessionId(), }, }); @@ -426,7 +422,7 @@ export async function mountApp( return () => { data.search.session.clear(); unmountComponentAtNode(params.element); - lensServices.inspector.close(); + lensServices.inspector.closeInspector(); unlistenParentHistory(); lensStore.dispatch(navigateAway()); stateTransfer.clearEditorState?.(APP_ID); diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.test.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.test.tsx new file mode 100644 index 0000000000000..987b320b3abf1 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.test.tsx @@ -0,0 +1,407 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SaveProps } from './app'; +import { type SaveVisualizationProps, runSaveLensVisualization } from './save_modal_container'; +import { defaultDoc, makeDefaultServices } from '../mocks'; +import faker from 'faker'; +import { makeAttributeService } from '../mocks/services_mock'; + +jest.mock('../persistence/saved_objects_utils/check_for_duplicate_title', () => ({ + checkForDuplicateTitle: jest.fn(async () => false), +})); + +describe('runSaveLensVisualization', () => { + // Need to call reset here as makeDefaultServices() reuses some mocks from core + const resetMocks = () => + beforeEach(() => { + jest.resetAllMocks(); + }); + + function getDefaultArgs( + servicesOverrides: Partial<SaveVisualizationProps> = {}, + { saveToLibrary, ...propsOverrides }: Partial<SaveProps & { saveToLibrary: boolean }> = {} + ) { + const redirectToOrigin = jest.fn(); + const redirectTo = jest.fn(); + const onAppLeave = jest.fn(); + const switchDatasource = jest.fn(); + const props: SaveVisualizationProps = { + ...makeDefaultServices(), + // start with both the initial input and lastKnownDoc synced + lastKnownDoc: defaultDoc, + initialInput: { attributes: defaultDoc, savedObjectId: defaultDoc.savedObjectId }, + redirectToOrigin, + redirectTo, + onAppLeave, + switchDatasource, + ...servicesOverrides, + }; + const saveProps: SaveProps = { + newTitle: faker.lorem.word(), + newDescription: faker.lorem.sentence(), + newTags: [faker.lorem.word(), faker.lorem.word()], + isTitleDuplicateConfirmed: false, + returnToOrigin: false, + dashboardId: undefined, + newCopyOnSave: false, + ...propsOverrides, + }; + const options = { + saveToLibrary: Boolean(saveToLibrary), + }; + + return { + props, + saveProps, + options, + // convenience shortcuts + /** + * This function will be called when a fresh chart is saved + * and in the modal the user chooses to add the chart into a specific dashboard. Make sure to pass the "dashboardId" prop as well to simulate this scenario. + * This is used to test indirectly the redirectToDashboard call + */ + redirectToDashboardFn: props.stateTransfer.navigateToWithEmbeddablePackage, + /** + * This function will be called before reloading the editor after saving a a new document/new copy of the document + */ + cleanupEditor: props.stateTransfer.clearEditorState, + saveToLibraryFn: props.attributeService.saveToLibrary, + toasts: props.notifications.toasts, + }; + } + + describe('from dashboard', () => { + describe('as by value', () => { + const defaultByValueDoc = { ...defaultDoc, savedObjectId: undefined }; + + describe('Save and return', () => { + resetMocks(); + + // Test the "Save and return" button + it('should get back to dashboard', async () => { + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } = + getDefaultArgs( + { + lastKnownDoc: defaultByValueDoc, + initialInput: { attributes: defaultByValueDoc }, + }, + { returnToOrigin: true } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(props.onAppLeave).toHaveBeenCalled(); + expect(props.redirectToOrigin).toHaveBeenCalled(); + + // callback not called + expect(redirectToDashboardFn).not.toHaveBeenCalled(); + expect(saveToLibraryFn).not.toHaveBeenCalled(); + expect(props.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); + + it('should get back to dashboard preserving the original panel settings', async () => { + const { props, saveProps, options } = getDefaultArgs( + { + lastKnownDoc: defaultByValueDoc, + initialInput: { + attributes: defaultByValueDoc, + title: 'blah', + timeRange: { from: 'now-7d', to: 'now' }, + }, + }, + { returnToOrigin: true } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(props.onAppLeave).toHaveBeenCalled(); + expect(props.redirectToOrigin).toHaveBeenCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ + title: 'blah', + timeRange: { from: 'now-7d', to: 'now' }, + }), + }) + ); + }); + }); + + describe('Save to library', () => { + resetMocks(); + + // Test the "Save to library" flow + it('should save to library without redirect', async () => { + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } = + getDefaultArgs( + { + lastKnownDoc: defaultByValueDoc, + initialInput: { attributes: defaultByValueDoc }, + }, + { + saveToLibrary: true, + // do not get back at dashboard once saved + returnToOrigin: false, + } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalled(); + expect(props.notifications.toasts.addSuccess).toHaveBeenCalled(); + + // not called + expect(props.onAppLeave).not.toHaveBeenCalled(); + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + expect(redirectToDashboardFn).not.toHaveBeenCalled(); + }); + + it('should save to library and redirect', async () => { + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } = + getDefaultArgs( + { + lastKnownDoc: defaultByValueDoc, + initialInput: { attributes: defaultByValueDoc }, + }, + { + saveToLibrary: true, + // return to dashboard once saved + returnToOrigin: true, + } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(props.onAppLeave).toHaveBeenCalled(); + expect(props.redirectToOrigin).toHaveBeenCalled(); + expect(saveToLibraryFn).toHaveBeenCalled(); + + // not called + expect(redirectToDashboardFn).not.toHaveBeenCalled(); + expect(props.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); + }); + }); + + describe('as by reference', () => { + resetMocks(); + // There are 4 possibilities here: + // save the current document overwriting the existing one + it('should overwrite and show a success toast', async () => { + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } = + getDefaultArgs( + { + // defaultDoc is by reference + }, + { newCopyOnSave: false, saveToLibrary: true } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + defaultDoc.savedObjectId + ); + expect(toasts.addSuccess).toHaveBeenCalled(); + + // not called + expect(props.onAppLeave).not.toHaveBeenCalled(); + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + expect(redirectToDashboardFn).not.toHaveBeenCalled(); + }); + + // save the current document as a new by-ref copy in the library + it('should save as a new copy and show a success toast', async () => { + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } = + getDefaultArgs( + { + // defaultDoc is by reference + }, + { newCopyOnSave: true, saveToLibrary: true } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + undefined + ); + expect(toasts.addSuccess).toHaveBeenCalled(); + + // not called + expect(props.onAppLeave).not.toHaveBeenCalled(); + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + expect(redirectToDashboardFn).not.toHaveBeenCalled(); + }); + // save the current document as a new by-value copy and add it to a dashboard + it('should save as a new by-value copy and redirect to the dashboard', async () => { + const dashboardId = faker.random.uuid(); + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } = + getDefaultArgs( + { + // defaultDoc is by reference + }, + { newCopyOnSave: true, saveToLibrary: false, dashboardId } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(props.onAppLeave).toHaveBeenCalled(); + + // not called + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + expect(redirectToDashboardFn).toHaveBeenCalledWith( + 'dashboards', + // make sure the new savedObject id is removed from the new input + expect.objectContaining({ + state: expect.objectContaining({ + input: expect.objectContaining({ savedObjectId: undefined }), + }), + }) + ); + expect(saveToLibraryFn).not.toHaveBeenCalled(); + expect(toasts.addSuccess).not.toHaveBeenCalled(); + }); + + // save the current document as a new by-ref copy and add it to a dashboard + it('should save as a new by-ref copy and redirect to the dashboard', async () => { + const dashboardId = faker.random.uuid(); + const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } = + getDefaultArgs( + { + // defaultDoc is by reference + }, + { newCopyOnSave: true, saveToLibrary: true, dashboardId } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(props.onAppLeave).toHaveBeenCalled(); + expect(redirectToDashboardFn).toHaveBeenCalledWith( + 'dashboards', + // make sure the new savedObject id is passed with the new input + expect.objectContaining({ + state: expect.objectContaining({ + input: expect.objectContaining({ savedObjectId: '1234' }), + }), + }) + ); + expect(saveToLibraryFn).toHaveBeenCalled(); + + // not called + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + expect(toasts.addSuccess).not.toHaveBeenCalled(); + }); + }); + }); + + describe('fresh editor start', () => { + resetMocks(); + + it('should reload the editor if it has been saved as new copy', async () => { + const { props, saveProps, options, saveToLibraryFn, cleanupEditor, toasts } = getDefaultArgs( + {}, + { + saveToLibrary: true, + newCopyOnSave: true, + } + ); + const result = await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalled(); + expect(toasts.addSuccess).toHaveBeenCalled(); + expect(cleanupEditor).toHaveBeenCalled(); + expect(props.redirectTo).toHaveBeenCalledWith(defaultDoc.savedObjectId); + expect(result?.isLinkedToOriginatingApp).toBeFalsy(); + + // not called + expect(props.onAppLeave).not.toHaveBeenCalled(); + }); + + it('should show a notification toast and reload as first save of the document', async () => { + const { props, saveProps, options, saveToLibraryFn, toasts } = getDefaultArgs( + { + lastKnownDoc: { ...defaultDoc, savedObjectId: undefined }, + persistedDoc: undefined, + initialInput: undefined, + }, + { saveToLibrary: true } + ); + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalled(); + expect(toasts.addSuccess).toHaveBeenCalled(); + expect(props.redirectTo).toHaveBeenCalled(); + + // not called + expect(props.application.navigateToApp).not.toHaveBeenCalledWith('lens', { path: '/' }); + expect(props.redirectToOrigin).not.toHaveBeenCalled(); + }); + + it('should throw if something goes wrong when saving', async () => { + const attributeServiceMock = { + ...makeAttributeService(defaultDoc), + saveToLibrary: jest.fn().mockImplementation(() => Promise.reject(Error('failed to save'))), + }; + const { props, saveProps, options, toasts } = getDefaultArgs( + { + lastKnownDoc: { ...defaultDoc, savedObjectId: undefined }, + attributeService: attributeServiceMock, + }, + { saveToLibrary: true } + ); + try { + await runSaveLensVisualization(props, saveProps, options); + } catch (error) { + expect(toasts.addDanger).toHaveBeenCalled(); + expect(toasts.addSuccess).not.toHaveBeenCalled(); + expect(error.message).toEqual('failed to save'); + } + }); + }); + + // While this is technically a virtual option as for now, it's still worth testing to not break it in the future + describe('Textbased version', () => { + resetMocks(); + + it('should have a dedicated flow for textbased saving by-ref', async () => { + // simulate a new save + const attributeServiceMock = makeAttributeService({ + ...defaultDoc, + savedObjectId: faker.random.uuid(), + }); + + const { props, saveProps, options, saveToLibraryFn, cleanupEditor } = getDefaultArgs( + { + textBasedLanguageSave: true, + attributeService: attributeServiceMock, + // give a document without a savedObjectId + lastKnownDoc: { ...defaultDoc, savedObjectId: undefined }, + persistedDoc: undefined, + // simulate a fresh start in the editor + initialInput: undefined, + }, + { + saveToLibrary: true, + } + ); + + await runSaveLensVisualization(props, saveProps, options); + + // callback called + expect(saveToLibraryFn).toHaveBeenCalled(); + expect(cleanupEditor).toHaveBeenCalled(); + expect(props.switchDatasource).toHaveBeenCalled(); + expect(props.redirectTo).not.toHaveBeenCalled(); + expect(props.application.navigateToApp).toHaveBeenCalledWith('lens', { path: '/' }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index 354bf0888259c..f1ccacc37db53 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -11,25 +11,29 @@ import { isFilterPinned } from '@kbn/es-query'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import type { SavedObjectReference } from '@kbn/core/public'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { omit } from 'lodash'; import { SaveModal } from './save_modal'; import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; -import { Document, checkForDuplicateTitle, SavedObjectIndexStore } from '../persistence'; -import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; +import { checkForDuplicateTitle, SavedObjectIndexStore, LensDocument } from '../persistence'; import { APP_ID, getFullPath } from '../../common/constants'; import type { LensAppState } from '../state_management'; -import { getPersisted } from '../state_management/init_middleware/load_initial'; -import { VisualizeEditorContext } from '../types'; +import { getFromPreloaded } from '../state_management/init_middleware/load_initial'; +import { Simplify, VisualizeEditorContext } from '../types'; import { redirectToDashboard } from './save_modal_container_helpers'; +import { LensSerializedState } from '../react_embeddable/types'; +import { isLegacyEditorEmbeddable } from './app_helpers'; -type ExtraProps = Pick<LensAppProps, 'initialInput'> & - Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>>; +type ExtraProps = Simplify< + Pick<LensAppProps, 'initialInput'> & + Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>> +>; export type SaveModalContainerProps = { originatingApp?: string; getOriginatingPath?: (dashboardId: string) => string; - persistedDoc?: Document; - lastKnownDoc?: Document; + persistedDoc?: LensDocument; + lastKnownDoc?: LensDocument; returnToOriginSwitchLabel?: string; onClose: () => void; onSave?: (saveProps: SaveProps) => void; @@ -78,19 +82,14 @@ export function SaveModalContainer({ let description; let savedObjectId; const [initializing, setInitializing] = useState(true); - const [lastKnownDoc, setLastKnownDoc] = useState<Document | undefined>(initLastKnownDoc); + const [lastKnownDoc, setLastKnownDoc] = useState<LensDocument | undefined>(initLastKnownDoc); if (lastKnownDoc) { title = lastKnownDoc.title; description = lastKnownDoc.description; savedObjectId = lastKnownDoc.savedObjectId; } - if ( - !lastKnownDoc?.title && - initialContext && - 'isEmbeddable' in initialContext && - initialContext.isEmbeddable - ) { + if (!lastKnownDoc?.title && isLegacyEditorEmbeddable(initialContext)) { title = i18n.translate('xpack.lens.app.convertedLabel', { defaultMessage: '{title} (converted)', values: { @@ -109,7 +108,7 @@ export function SaveModalContainer({ let isMounted = true; if (initialInput) { - getPersisted({ + getFromPreloaded({ initialInput, lensServices, }) @@ -133,12 +132,13 @@ export function SaveModalContainer({ ? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references) : []; - const runLensSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { + const runLensSave = async (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { if (runSave) { // inside lens, we use the function that's passed to it - runSave(saveProps, options); - } else if (attributeService && lastKnownDoc) { - runSaveLensVisualization( + return runSave(saveProps, options); + } + if (attributeService && lastKnownDoc) { + await runSaveLensVisualization( { ...lensServices, lastKnownDoc, @@ -147,16 +147,14 @@ export function SaveModalContainer({ redirectToOrigin, originatingApp, getOriginatingPath, - getIsByValueMode: () => false, onAppLeave: () => {}, ...lensServices, }, saveProps, options - ).then(() => { - onSave?.(saveProps); - onClose(); - }); + ); + onSave?.(saveProps); + onClose(); } }; @@ -188,11 +186,24 @@ export function SaveModalContainer({ ); } +function fromDocumentToSerializedState( + doc: LensDocument, + panelSettings: Partial<LensSerializedState>, + originalInput?: LensAppProps['initialInput'] +): LensSerializedState { + return { + ...originalInput, + attributes: omit(doc, 'savedObjectId'), + savedObjectId: doc.savedObjectId, + ...panelSettings, + }; +} + const getDocToSave = ( - lastKnownDoc: Document, + lastKnownDoc: LensDocument, saveProps: SaveProps, references: SavedObjectReference[] -) => { +): LensDocument => { const docToSave = { ...removePinnedFilters(lastKnownDoc)!, references, @@ -209,11 +220,10 @@ const getDocToSave = ( return docToSave; }; -export const runSaveLensVisualization = async ( - props: { - lastKnownDoc?: Document; - getIsByValueMode: () => boolean; - persistedDoc?: Document; +export type SaveVisualizationProps = Simplify< + { + lastKnownDoc?: LensDocument; + persistedDoc?: LensDocument; originatingApp?: string; getOriginatingPath?: (dashboardId: string) => string; textBasedLanguageSave?: boolean; @@ -232,7 +242,11 @@ export const runSaveLensVisualization = async ( | 'stateTransfer' | 'attributeService' | 'savedObjectsTagging' - >, + > +>; + +export const runSaveLensVisualization = async ( + props: SaveVisualizationProps, saveProps: SaveProps, options: { saveToLibrary: boolean } ): Promise<Partial<LensAppState> | undefined> => { @@ -245,7 +259,6 @@ export const runSaveLensVisualization = async ( stateTransfer, attributeService, savedObjectsTagging, - getIsByValueMode, redirectToOrigin, onAppLeave, redirectTo, @@ -262,7 +275,7 @@ export const runSaveLensVisualization = async ( return; } - let references = lastKnownDoc.references; + let references = lastKnownDoc.references || initialInput?.attributes?.references; if (savedObjectsTagging) { const tagsIds = @@ -277,68 +290,90 @@ export const runSaveLensVisualization = async ( const docToSave = getDocToSave(lastKnownDoc, saveProps, references); - // Required to serialize filters in by value mode until - // https://github.com/elastic/kibana/issues/77588 is fixed - if (getIsByValueMode()) { - docToSave.state.filters.forEach((filter) => { - if (typeof filter.meta.value === 'function') { - delete filter.meta.value; - } - }); - } - const originalInput = saveProps.newCopyOnSave ? undefined : initialInput; - const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId; + const originalSavedObjectId = originalInput?.savedObjectId; if (options.saveToLibrary) { - try { - await checkForDuplicateTitle( - { - id: originalSavedObjectId, - title: docToSave.title, - displayName: i18n.translate('xpack.lens.app.saveModalType', { - defaultMessage: 'Lens visualization', - }), - lastSavedTitle: lastKnownDoc.title, - copyOnSave: saveProps.newCopyOnSave, - isTitleDuplicateConfirmed: saveProps.isTitleDuplicateConfirmed, - }, - saveProps.onTitleDuplicate, - { - client: savedObjectStore, - ...startServices, - } - ); - } catch (e) { - // ignore duplicate title failure, user notified in save modal - throw e; - } + // this is a lower level call that the Lens attribute service one + // @TODO: check if it's worth to replace it witht he attribute service one + await checkForDuplicateTitle( + { + id: originalSavedObjectId, + title: docToSave.title, + displayName: i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + }), + lastSavedTitle: lastKnownDoc.title, + copyOnSave: saveProps.newCopyOnSave, + isTitleDuplicateConfirmed: saveProps.isTitleDuplicateConfirmed, + }, + saveProps.onTitleDuplicate, + { + client: savedObjectStore, + ...startServices, + } + ); + // ignore duplicate title failure, user notified in save modal } + try { - let newInput = (await attributeService.wrapAttributes( + // wrap the doc into a serializable state + const newDoc = fromDocumentToSerializedState( docToSave, - options.saveToLibrary, + { + timeRange: saveProps.panelTimeRange ?? originalInput?.timeRange, + savedObjectId: options.saveToLibrary ? originalSavedObjectId : undefined, + }, originalInput - )) as LensEmbeddableInput; - if (saveProps.panelTimeRange) { - newInput = { - ...newInput, - timeRange: saveProps.panelTimeRange, - }; + ); + + let savedObjectId: string | undefined; + try { + savedObjectId = + newDoc.attributes && options.saveToLibrary + ? await attributeService.saveToLibrary( + newDoc.attributes, + newDoc.attributes.references || [], + originalSavedObjectId + ) + : undefined; + } catch (error) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.lens.app.saveVisualization.errorNotificationText', { + defaultMessage: `An error occurred while saving. Error: {errorMessage}`, + values: { + errorMessage: error.message, + }, + }), + }); + // trigger a reject to jump to the final catch clause + throw error; } - if (saveProps.returnToOrigin && redirectToOrigin) { + + const shouldNavigateBackToOrigin = saveProps.returnToOrigin && redirectToOrigin; + const hasRedirect = shouldNavigateBackToOrigin || saveProps.dashboardId; + + // if a redirect was set, prevent the validation on app leave + if (hasRedirect) { // disabling the validation on app leave because the document has been saved. onAppLeave?.((actions) => { return actions.default(); }); - redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave }); - return; - } else if (saveProps.dashboardId) { - // disabling the validation on app leave because the document has been saved. - onAppLeave?.((actions) => { - return actions.default(); + } + + if (shouldNavigateBackToOrigin) { + redirectToOrigin({ + state: { ...newDoc, savedObjectId }, + isCopied: saveProps.newCopyOnSave, }); + return; + } + // should we make it more robust here and better check the context of the saving + // or keep the responsability of the consumer of the function to provide the right set + // of args here in case the user is within a by value chart AND want's to save it in the library + // without redirect? + if (saveProps.dashboardId) { redirectToDashboard({ - embeddableInput: newInput, + embeddableInput: { ...newDoc, savedObjectId }, dashboardId: saveProps.dashboardId, stateTransfer, originatingApp: props.originatingApp, @@ -356,15 +391,8 @@ export const runSaveLensVisualization = async ( }) ); - if ( - attributeService.inputIsRefType(newInput) && - newInput.savedObjectId !== originalSavedObjectId - ) { - chrome.recentlyAccessed.add( - getFullPath(newInput.savedObjectId), - docToSave.title, - newInput.savedObjectId - ); + if (savedObjectId && savedObjectId !== originalSavedObjectId) { + chrome.recentlyAccessed.add(getFullPath(savedObjectId), docToSave.title, savedObjectId); // remove editor state so the connection is still broken after reload stateTransfer.clearEditorState?.(APP_ID); @@ -372,18 +400,13 @@ export const runSaveLensVisualization = async ( switchDatasource?.(); application.navigateToApp('lens', { path: '/' }); } else { - redirectTo?.(newInput.savedObjectId); + redirectTo?.(savedObjectId); } return { isLinkedToOriginatingApp: false }; } - const newDoc = { - ...docToSave, - ...newInput, - }; - return { - persistedDoc: newDoc, + persistedDoc: newDoc.attributes, isLinkedToOriginatingApp: false, }; } catch (e) { @@ -393,7 +416,7 @@ export const runSaveLensVisualization = async ( } }; -export function removePinnedFilters(doc?: Document) { +export function removePinnedFilters(doc?: LensDocument) { if (!doc) return undefined; return { ...doc, diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.test.ts b/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.test.ts index 1f4e255c54414..9415ab2e323cd 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.test.ts @@ -5,14 +5,14 @@ * 2.0. */ import { makeDefaultServices } from '../mocks'; -import type { LensEmbeddableInput } from '../embeddable'; import type { LensAppServices } from './types'; import { redirectToDashboard } from './save_modal_container_helpers'; +import { LensSerializedState } from '..'; describe('redirectToDashboard', () => { const embeddableInput = { test: 'test', - } as unknown as LensEmbeddableInput; + } as unknown as LensSerializedState; const mockServices = makeDefaultServices(); it('should call the navigateToWithEmbeddablePackage with the correct args if originatingApp is given', () => { diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.ts b/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.ts index 98b2d0bdc2aba..44b879c7f27cb 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.ts +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container_helpers.ts @@ -6,8 +6,8 @@ */ import type { LensAppServices } from './types'; -import type { LensEmbeddableInput } from '../embeddable'; import { LENS_EMBEDDABLE_TYPE } from '../../common/constants'; +import { LensSerializedState } from '../react_embeddable/types'; export const redirectToDashboard = ({ embeddableInput, @@ -16,7 +16,7 @@ export const redirectToDashboard = ({ getOriginatingPath, stateTransfer, }: { - embeddableInput: LensEmbeddableInput; + embeddableInput: LensSerializedState; dashboardId: string; originatingApp?: string; getOriginatingPath?: (dashboardId: string) => string | undefined; diff --git a/x-pack/plugins/lens/public/app_plugin/share_action.ts b/x-pack/plugins/lens/public/app_plugin/share_action.ts index c9ec3a11ef5e7..dbb5d9d61eda9 100644 --- a/x-pack/plugins/lens/public/app_plugin/share_action.ts +++ b/x-pack/plugins/lens/public/app_plugin/share_action.ts @@ -11,7 +11,7 @@ import { DataViewSpec } from '@kbn/data-views-plugin/common'; import type { LensAppLocatorParams } from '../../common/locator/locator'; import type { LensAppState } from '../state_management'; import type { LensAppServices } from './types'; -import type { Document } from '../persistence/saved_object_store'; +import type { LensDocument } from '../persistence/saved_object_store'; import type { DatasourceMap, VisualizationMap } from '../types'; import { extractReferencesFromState, getResolvedDateRange } from '../utils'; import { getEditPath } from '../../common/constants'; @@ -23,7 +23,7 @@ interface ShareableConfiguration > { datasourceMap: DatasourceMap; visualizationMap: VisualizationMap; - currentDoc: Document | undefined; + currentDoc: LensDocument | undefined; adHocDataViews?: DataViewSpec[]; } @@ -37,7 +37,7 @@ export const DEFAULT_LENS_LAYOUT_DIMENSIONS = { function getShareURLForSavedObject( { application, data }: Pick<LensAppServices, 'application' | 'data'>, - currentDoc: Document | undefined + currentDoc: LensDocument | undefined ) { return new URL( `${application.getUrlForApp('lens', { absolute: true })}${ @@ -89,7 +89,7 @@ export function getLocatorParams( const serializableDatasourceStates = datasourceStates as LensAppState['datasourceStates'] & SerializableRecord; - const snapshotParams = { + const snapshotParams: LensAppLocatorParams = { filters, query, resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx index 205aa74aaee24..dedd34c24cb53 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx @@ -16,6 +16,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { isEqual } from 'lodash'; import { RootDragDropProvider } from '@kbn/dom-drag-drop'; +import { TypedLensSerializedState } from '../../../react_embeddable/types'; import type { LensPluginStartDependencies } from '../../../plugin'; import { makeConfigureStore, @@ -28,8 +29,7 @@ import { generateId } from '../../../id_generator'; import type { DatasourceMap, VisualizationMap } from '../../../types'; import { LensEditConfigurationFlyout } from './lens_configuration_flyout'; import type { EditConfigPanelProps } from './types'; -import { SavedObjectIndexStore, type Document } from '../../../persistence'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import { SavedObjectIndexStore, type LensDocument } from '../../../persistence'; import { DOC_TYPE } from '../../../../common/constants'; export type EditLensConfigurationProps = Omit< @@ -87,6 +87,41 @@ export const updatingMiddleware = } }; +const MaybeWrapper = ({ + wrapInFlyout, + closeFlyout, + children, +}: { + wrapInFlyout?: boolean; + children: JSX.Element; + closeFlyout?: () => void; +}) => { + if (!wrapInFlyout) { + return children; + } + return ( + <EuiFlyout + data-test-subj="lnsEditOnFlyFlyout" + type="push" + ownFocus + paddingSize="m" + onClose={() => { + closeFlyout?.(); + }} + aria-labelledby={i18n.translate('xpack.lens.config.editLabel', { + defaultMessage: 'Edit configuration', + })} + size="s" + hideCloseButton + css={css` + clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%); + `} + > + {children} + </EuiFlyout> + ); +}; + export async function getEditLensConfiguration( coreStart: CoreStart, startDependencies: LensPluginStartDependencies, @@ -109,30 +144,29 @@ export async function getEditLensConfiguration( datasourceId, panelId, savedObjectId, - output$, + dataLoading$, lensAdapters, updateByRefInput, navigateToLensEditor, displayFlyoutHeader, canEditTextBasedQuery, isNewPanel, - deletePanel, hidesSuggestions, - onApplyCb, - onCancelCb, + onApply, + onCancel, hideTimeFilterInfo, }: EditLensConfigurationProps) => { if (!lensServices || !datasourceMap || !visualizationMap) { return <LoadingSpinnerWithOverlay />; } const [currentAttributes, setCurrentAttributes] = - useState<TypedLensByValueInput['attributes']>(attributes); + useState<TypedLensSerializedState['attributes']>(attributes); /** * During inline editing of a by reference panel, the panel is converted to a by value one. * When the user applies the changes we save them to the Lens SO */ const saveByRef = useCallback( - async (attrs: Document) => { + async (attrs: LensDocument) => { const savedObjectStore = new SavedObjectIndexStore(lensServices.contentManagement); await savedObjectStore.save({ ...attrs, @@ -167,34 +201,6 @@ export async function getEditLensConfiguration( }) ); - const getWrapper = (children: JSX.Element) => { - if (wrapInFlyout) { - return ( - <EuiFlyout - data-test-subj="lnsEditOnFlyFlyout" - type="push" - ownFocus - paddingSize="m" - onClose={() => { - closeFlyout?.(); - }} - aria-labelledby={i18n.translate('xpack.lens.config.editLabel', { - defaultMessage: 'Edit configuration', - })} - size="s" - hideCloseButton - css={css` - clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%); - `} - > - {children} - </EuiFlyout> - ); - } else { - return children; - } - }; - const configPanelProps = { attributes: currentAttributes, updatePanelState, @@ -204,7 +210,7 @@ export async function getEditLensConfiguration( coreStart, startDependencies, visualizationMap, - output$, + dataLoading$, lensAdapters, datasourceMap, saveByRef, @@ -216,22 +222,23 @@ export async function getEditLensConfiguration( hidesSuggestions, setCurrentAttributes, isNewPanel, - deletePanel, - onApplyCb, - onCancelCb, + onApply, + onCancel, hideTimeFilterInfo, }; - return getWrapper( - <Provider store={lensStore}> - <KibanaRenderContextProvider {...coreStart}> - <KibanaContextProvider services={lensServices}> - <RootDragDropProvider> - <LensEditConfigurationFlyout {...configPanelProps} /> - </RootDragDropProvider> - </KibanaContextProvider> - </KibanaRenderContextProvider> - </Provider> + return ( + <MaybeWrapper wrapInFlyout={wrapInFlyout} closeFlyout={closeFlyout}> + <Provider store={lensStore}> + <KibanaRenderContextProvider {...coreStart}> + <KibanaContextProvider services={lensServices}> + <RootDragDropProvider> + <LensEditConfigurationFlyout {...configPanelProps} /> + </RootDragDropProvider> + </KibanaContextProvider> + </KibanaRenderContextProvider> + </Provider> + </MaybeWrapper> ); }; } diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts index 1274008d0de88..c0280af595041 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts @@ -18,7 +18,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { getTime } from '@kbn/data-plugin/common'; import { type DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import { TypedLensSerializedState } from '../../../react_embeddable/types'; import type { LensPluginStartDependencies } from '../../../plugin'; import type { DatasourceMap, VisualizationMap } from '../../../types'; import { suggestionsApi } from '../../../lens_suggestions_api'; @@ -123,7 +123,7 @@ export const getSuggestions = async ( query, suggestion: firstSuggestion, dataView, - }) as TypedLensByValueInput['attributes']; + }) as TypedLensSerializedState['attributes']; return attrs; } catch (e) { setErrors([e]); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx index 85c7036a3e9df..474d5cc69c188 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx @@ -13,9 +13,9 @@ import { coreMock } from '@kbn/core/public/mocks'; import { mockVisualizationMap, mockDatasourceMap, mockDataPlugin } from '../../../mocks'; import type { LensPluginStartDependencies } from '../../../plugin'; import { createMockStartDependencies } from '../../../editor_frame_service/mocks'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; import { LensEditConfigurationFlyout } from './lens_configuration_flyout'; import type { EditConfigPanelProps } from './types'; +import { TypedLensSerializedState } from '../../../react_embeddable/types'; jest.mock('@kbn/esql-utils', () => { return { @@ -93,7 +93,7 @@ const lensAttributes = { esql: 'from index1 | limit 10', }, references: [], -} as unknown as TypedLensByValueInput['attributes']; +} as unknown as TypedLensSerializedState['attributes']; const mockStartDependencies = createMockStartDependencies() as unknown as LensPluginStartDependencies; @@ -139,6 +139,8 @@ describe('LensEditConfigurationFlyout', () => { visualizationMap={visualizationMap} closeFlyout={jest.fn()} datasourceId={'testDatasource' as EditConfigPanelProps['datasourceId']} + onApply={jest.fn()} + onCancel={jest.fn()} {...propsOverrides} />, {}, @@ -234,7 +236,7 @@ describe('LensEditConfigurationFlyout', () => { await renderConfigFlyout( { closeFlyout: jest.fn(), - onApplyCb: onApplyCbSpy, + onApply: onApplyCbSpy, }, { esql: 'from index1 | limit 10' } ); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index fd3bcdc8bed8a..8c8693cd7c76d 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -30,6 +30,7 @@ import { import type { AggregateQuery, Query } from '@kbn/es-query'; import { ESQLLangEditor } from '@kbn/esql/public'; import { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import type { TypedLensSerializedState } from '../../../react_embeddable/types'; import { buildExpression } from '../../../editor_frame_service/editor_frame/expression_helpers'; import { MAX_NUM_OF_COLUMNS } from '../../../datasources/text_based/utils'; import { @@ -38,7 +39,6 @@ import { onActiveDataChange, useLensDispatch, } from '../../../state_management'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; import { EXPRESSION_BUILD_ERROR_ID, extractReferencesFromState, @@ -67,20 +67,19 @@ export function LensEditConfigurationFlyout({ saveByRef, savedObjectId, updateByRefInput, - output$, + dataLoading$, lensAdapters, navigateToLensEditor, displayFlyoutHeader, canEditTextBasedQuery, isNewPanel, - deletePanel, hidesSuggestions, - onApplyCb, - onCancelCb, + onApply: onApplyCallback, + onCancel: onCancelCallback, hideTimeFilterInfo, }: EditConfigPanelProps) { const euiTheme = useEuiTheme(); - const previousAttributes = useRef<TypedLensByValueInput['attributes']>(attributes); + const previousAttributes = useRef<TypedLensSerializedState['attributes']>(attributes); const previousAdapters = useRef<Partial<DefaultInspectorAdapters> | undefined>(lensAdapters); const prevQuery = useRef<AggregateQuery | Query>(attributes.state.query); const [query, setQuery] = useState<AggregateQuery | Query>(attributes.state.query); @@ -117,7 +116,11 @@ export function LensEditConfigurationFlyout({ const dispatch = useLensDispatch(); useEffect(() => { - const s = output$?.subscribe(() => { + const s = dataLoading$?.subscribe((isDataLoading) => { + // go thru only when the loading is complete + if (isDataLoading) { + return; + } const activeData: Record<string, Datatable> = {}; const adaptersTables = previousAdapters.current?.tables?.tables; const [table] = Object.values(adaptersTables || {}); @@ -134,7 +137,7 @@ export function LensEditConfigurationFlyout({ } }); return () => s?.unsubscribe(); - }, [dispatch, output$, layers]); + }, [dispatch, dataLoading$, layers]); useEffect(() => { const abortController = new AbortController(); @@ -217,16 +220,10 @@ export function LensEditConfigurationFlyout({ updateByRefInput?.(savedObjectId); } } - // for a newly created chart, I want cancelling to also remove the panel - if (isNewPanel && deletePanel) { - deletePanel(); - } - onCancelCb?.(); + onCancelCallback?.(); closeFlyout?.(); }, [ attributesChanged, - isNewPanel, - deletePanel, closeFlyout, visualization.activeId, savedObjectId, @@ -235,7 +232,7 @@ export function LensEditConfigurationFlyout({ updatePanelState, updateSuggestion, updateByRefInput, - onCancelCb, + onCancelCallback, ]); const textBasedMode = useMemo( @@ -244,6 +241,9 @@ export function LensEditConfigurationFlyout({ ); const onApply = useCallback(() => { + if (visualization.activeId == null) { + return; + } const dsStates = Object.fromEntries( Object.entries(datasourceStates).map(([id, ds]) => { const dsState = ds.state; @@ -265,7 +265,7 @@ export function LensEditConfigurationFlyout({ activeVisualization, }) : []; - const attrs = { + const attrs: TypedLensSerializedState['attributes'] = { ...attributes, state: { ...attributes.state, @@ -293,18 +293,18 @@ export function LensEditConfigurationFlyout({ trackSaveUiCounterEvents(telemetryEvents); } - onApplyCb?.(attrs as TypedLensByValueInput['attributes']); + onApplyCallback?.(attrs); closeFlyout?.(); }, [ + visualization.activeId, + savedObjectId, + closeFlyout, + onApplyCallback, datasourceStates, textBasedMode, visualization.state, - visualization.activeId, activeVisualization, attributes, - savedObjectId, - onApplyCb, - closeFlyout, datasourceMap, saveByRef, updateByRefInput, diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts index d2aceb323773a..d31a518cf80e8 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import type { PublishingSubject } from '@kbn/presentation-publishing'; +import type { TypedLensSerializedState } from '../../../react_embeddable/types'; import type { LensPluginStartDependencies } from '../../../plugin'; import type { DatasourceMap, @@ -14,9 +14,8 @@ import type { FramePublicAPI, UserMessagesGetter, } from '../../../types'; -import type { LensEmbeddableOutput } from '../../../embeddable'; import type { LensInspector } from '../../../lens_inspector_service'; -import type { Document } from '../../../persistence'; +import type { LensDocument } from '../../../persistence'; export interface FlyoutWrapperProps { children: JSX.Element; @@ -37,22 +36,22 @@ export interface EditConfigPanelProps { visualizationMap: VisualizationMap; datasourceMap: DatasourceMap; /** The attributes of the Lens embeddable */ - attributes: TypedLensByValueInput['attributes']; + attributes: TypedLensSerializedState['attributes']; /** Callback for updating the visualization and datasources state.*/ updatePanelState: ( datasourceState: unknown, visualizationState: unknown, - visualizationType?: string + visualizationId?: string ) => void; - updateSuggestion?: (attrs: TypedLensByValueInput['attributes']) => void; + updateSuggestion?: (attrs: TypedLensSerializedState['attributes']) => void; /** Set the attributes state */ - setCurrentAttributes?: (attrs: TypedLensByValueInput['attributes']) => void; + setCurrentAttributes?: (attrs: TypedLensSerializedState['attributes']) => void; /** Lens visualizations can be either created from ESQL (textBased) or from dataviews (formBased) */ datasourceId: 'formBased' | 'textBased'; /** Embeddable output observable, useful for dashboard flyout */ - output$?: Observable<LensEmbeddableOutput>; + dataLoading$?: PublishingSubject<boolean | undefined>; /** Contains the active data, necessary for some panel configuration such as coloring */ - lensAdapters?: LensInspector['adapters']; + lensAdapters?: ReturnType<LensInspector['getInspectorAdapters']>; /** Optional callback called when updating the by reference embeddable */ updateByRefInput?: (soId: string) => void; /** Callback for closing the edit flyout */ @@ -69,7 +68,7 @@ export interface EditConfigPanelProps { */ savedObjectId?: string; /** Callback for saving the embeddable as a SO */ - saveByRef?: (attrs: Document) => void; + saveByRef?: (attrs: LensDocument) => void; /** Optional callback for navigation from the header of the flyout */ navigateToLensEditor?: () => void; /** If set to true it displays a header on the flyout */ @@ -78,21 +77,19 @@ export interface EditConfigPanelProps { canEditTextBasedQuery?: boolean; /** The flyout is used for adding a new panel by scratch */ isNewPanel?: boolean; - /** Handler for deleting the embeddable, used in case a user cancels a newly created chart */ - deletePanel?: () => void; /** If set to true the layout changes to accordion and the text based query (i.e. ES|QL) can be edited */ hidesSuggestions?: boolean; - /** Optional callback for apply flyout button */ - onApplyCb?: (input: TypedLensByValueInput['attributes']) => void; - /** Optional callback for cancel flyout button */ - onCancelCb?: () => void; + /** Apply button handler */ + onApply?: (attrs: TypedLensSerializedState['attributes']) => void; + /** Cancel button handler */ + onCancel?: () => void; // in cases where the embeddable is not filtered by time // (e.g. through unified search) set this property to true hideTimeFilterInfo?: boolean; } export interface LayerConfigurationProps { - attributes: TypedLensByValueInput['attributes']; + attributes: TypedLensSerializedState['attributes']; coreStart: CoreStart; startDependencies: LensPluginStartDependencies; visualizationMap: VisualizationMap; diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index fa9268c0374eb..f35443a510147 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -16,6 +16,7 @@ import { EsQueryConfig, isOfQueryType, AggregateQuery, + isOfAggregateQueryType, } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; @@ -219,8 +220,9 @@ export function combineQueryAndFilters( }; const allQueries = Array.isArray(query) ? query : query && isOfQueryType(query) ? [query] : []; - const nonEmptyQueries = allQueries.filter((q) => - Boolean(typeof q.query === 'string' ? q.query.trim() : q.query) + const nonEmptyQueries = allQueries.filter( + (q) => + !isOfAggregateQueryType(q) && Boolean(typeof q.query === 'string' ? q.query.trim() : q.query) ); [queries.lucene, queries.kuery] = partition(nonEmptyQueries, (q) => q.language === 'lucene'); diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 317efd5be507f..4791dc89d446f 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -55,15 +55,15 @@ import type { UserMessagesGetter, StartServices, } from '../types'; -import type { LensAttributeService } from '../lens_attribute_service'; -import type { LensEmbeddableInput } from '../embeddable/embeddable'; +import type { LensAttributesService } from '../lens_attribute_service'; import type { LensInspector } from '../lens_inspector_service'; import type { IndexPatternServiceAPI } from '../data_views_service/service'; -import type { Document, SavedObjectIndexStore } from '../persistence/saved_object_store'; +import type { LensDocument, SavedObjectIndexStore } from '../persistence/saved_object_store'; import type { LensAppLocator, LensAppLocatorParams } from '../../common/locator/locator'; +import { LensSerializedState } from '../react_embeddable/types'; export interface RedirectToOriginProps { - input?: LensEmbeddableInput; + state?: LensSerializedState; isCopied?: boolean; } @@ -76,7 +76,7 @@ export interface LensAppProps { redirectToOrigin?: (props?: RedirectToOriginProps) => void; // The initial input passed in by the container when editing. Can be either by reference or by value. - initialInput?: LensEmbeddableInput; + initialInput?: LensSerializedState; // State passed in by the container which is used to determine the id of the Originating App. incomingState?: EmbeddableEditorState; @@ -110,7 +110,7 @@ export interface LensTopNavMenuProps { redirectToOrigin?: (props?: RedirectToOriginProps) => void; // The initial input passed in by the container when editing. Can be either by reference or by value. - initialInput?: LensEmbeddableInput; + initialInput?: LensSerializedState; getIsByValueMode: () => boolean; indicateNoData: boolean; setIsSaveModalVisible: React.Dispatch<React.SetStateAction<boolean>>; @@ -124,7 +124,7 @@ export interface LensTopNavMenuProps { initialContextIsEmbedded?: boolean; topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; initialContext?: VisualizeFieldContext | VisualizeEditorContext; - currentDoc: Document | undefined; + currentDoc: LensDocument | undefined; indexPatternService: IndexPatternServiceAPI; getUserMessages: UserMessagesGetter; shortUrlService: (params: LensAppLocatorParams) => Promise<string>; @@ -156,7 +156,7 @@ export interface LensAppServices extends StartServices { usageCollection?: UsageCollectionStart; stateTransfer: EmbeddableStateTransfer; navigation: NavigationPublicPluginStart; - attributeService: LensAttributeService; + attributeService: LensAttributesService; contentManagement: ContentManagementPublicStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index 28becae5e6071..e5523b38b525d 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -43,13 +43,11 @@ export * from './lens_ui_telemetry'; export * from './lens_ui_errors'; export * from './editor_frame_service/editor_frame'; export * from './editor_frame_service'; -export * from './embeddable'; export * from './app_plugin/mounter'; export * from './lens_attribute_service'; export * from './app_plugin/save_modal_container'; export * from './chart_info_api'; export * from './trigger_actions/open_in_discover_helpers'; -export * from './trigger_actions/open_lens_config/edit_action_helpers'; export * from './trigger_actions/open_lens_config/create_action_helpers'; export * from './trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers'; diff --git a/x-pack/plugins/lens/public/chart_info_api.test.ts b/x-pack/plugins/lens/public/chart_info_api.test.ts index c302d4e934eba..f647e2289c5bf 100644 --- a/x-pack/plugins/lens/public/chart_info_api.test.ts +++ b/x-pack/plugins/lens/public/chart_info_api.test.ts @@ -6,9 +6,9 @@ */ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import type { EditorFrameService } from './editor_frame_service'; import { createChartInfoApi } from './chart_info_api'; -import type { LensSavedObjectAttributes } from '.'; +import { LensDocument } from './persistence'; +import { DatasourceMap, VisualizationMap } from './types'; const mockGetVisualizationInfo = jest.fn().mockReturnValue({ layers: [ @@ -37,18 +37,19 @@ const mockGetDatasourceInfo = jest.fn().mockResolvedValue([ describe('createChartInfoApi', () => { const dataViews = dataViewPluginMocks.createStartContract(); test('get correct chart info', async () => { - const chartInfoApi = await createChartInfoApi(dataViews, { - loadVisualizations: () => ({ + const chartInfoApi = await createChartInfoApi( + dataViews, + { lnsXY: { getVisualizationInfo: mockGetVisualizationInfo, }, - }), - loadDatasources: () => ({ + } as unknown as VisualizationMap, + { from_based: { getDatasourceInfo: mockGetDatasourceInfo, }, - }), - } as unknown as EditorFrameService); + } as unknown as DatasourceMap + ); const vis = { title: 'xy', visualizationType: 'lnsXY', @@ -69,7 +70,7 @@ describe('createChartInfoApi', () => { query: '', }, references: [], - } as LensSavedObjectAttributes; + } as LensDocument; const chartInfo = await chartInfoApi.getChartInfo(vis); diff --git a/x-pack/plugins/lens/public/chart_info_api.ts b/x-pack/plugins/lens/public/chart_info_api.ts index d2661226cdf1f..ace9ab445dba6 100644 --- a/x-pack/plugins/lens/public/chart_info_api.ts +++ b/x-pack/plugins/lens/public/chart_info_api.ts @@ -5,23 +5,22 @@ * 2.0. */ -import type { Filter, Query } from '@kbn/es-query'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { IconType } from '@elastic/eui/src/components/icon/icon'; import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { getActiveDatasourceIdFromDoc } from './utils'; -import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; -import type { OperationDescriptor } from './types'; -import type { LensSavedObjectAttributes } from '.'; +import type { DatasourceMap, OperationDescriptor, VisualizationMap } from './types'; +import { LensDocument } from './persistence'; export type ChartInfoApi = Promise<{ - getChartInfo: (vis: LensSavedObjectAttributes) => Promise<ChartInfo | undefined>; + getChartInfo: (vis: LensDocument) => Promise<ChartInfo | undefined>; }>; export interface ChartInfo { layers: ChartLayerDescriptor[]; visualizationType: string; filters: Filter[]; - query: Query; + query: Query | AggregateQuery; } export interface ChartLayerDescriptor { @@ -42,17 +41,14 @@ export interface ChartLayerDescriptor { export const createChartInfoApi = async ( dataViews: DataViewsPublicPluginStart, - editorFrameService?: EditorFrameServiceType + visualizationMap: VisualizationMap, + datasourceMap: DatasourceMap ): ChartInfoApi => { - const [visualizationMap, datasourceMap] = await Promise.all([ - editorFrameService!.loadVisualizations(), - editorFrameService!.loadDatasources(), - ]); return { - async getChartInfo(vis: LensSavedObjectAttributes): Promise<ChartInfo | undefined> { + async getChartInfo(vis: LensDocument): Promise<ChartInfo | undefined> { const lensVis = vis; const activeDatasourceId = getActiveDatasourceIdFromDoc(lensVis); - if (!activeDatasourceId || !lensVis?.visualizationType) { + if (!activeDatasourceId || lensVis?.visualizationType == null) { return undefined; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index e356a59956f06..ff51014f548d3 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -12,6 +12,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CoreStart } from '@kbn/core/public'; +import { Query } from '@kbn/es-query'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { type DataView, DataViewField, FieldSpec } from '@kbn/data-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -42,7 +43,7 @@ import { IndexPatternServiceAPI } from '../../data_views_service/service'; import { FieldItem } from '../common/field_item'; export type FormBasedDataPanelProps = Omit< - DatasourceDataPanelProps<FormBasedPrivateState>, + DatasourceDataPanelProps<FormBasedPrivateState, Query>, 'core' | 'onChangeIndexPattern' > & { data: DataPublicPluginStart; @@ -185,7 +186,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ showNoDataPopover, activeIndexPatterns, }: Omit< - DatasourceDataPanelProps, + DatasourceDataPanelProps<unknown, Query>, 'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns' > & { data: DataPublicPluginStart; diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts index b399f8eaa7b54..cd26abe0fdd86 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts @@ -51,6 +51,7 @@ import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common'; import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages'; import { createMockFramePublicAPI } from '../../mocks'; import { createMockDataViewsState } from '../../data_views_service/mocks'; +import { Query } from '@kbn/es-query'; jest.mock('./loader'); jest.mock('../../id_generator'); @@ -193,7 +194,7 @@ const dateRange = { describe('IndexPattern Data Source', () => { let baseState: FormBasedPrivateState; - let FormBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState>; + let FormBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState, Query>; beforeEach(() => { const data = dataPluginMock.createStartContract(); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index da893707ab2bc..ebe98c56adebf 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -8,7 +8,7 @@ import React from 'react'; import type { CoreStart, SavedObjectReference } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; +import { Query, TimeRange } from '@kbn/es-query'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { flatten, isEqual } from 'lodash'; @@ -28,7 +28,6 @@ import memoizeOne from 'memoize-one'; import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, - DatasourceDataPanelProps, DatasourceLayerPanelProps, PublicAPIProps, OperationDescriptor, @@ -40,6 +39,7 @@ import type { UserMessage, StateSetter, IndexPatternMap, + DatasourceDataPanelProps, } from '../../types'; import { changeIndexPattern, @@ -217,7 +217,7 @@ export function getFormBasedDatasource({ const ALIAS_IDS = ['indexpattern']; // Not stateful. State is persisted to the frame - const formBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState> = { + const formBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState, Query> = { id: DATASOURCE_ID, alias: ALIAS_IDS, @@ -464,7 +464,7 @@ export function getFormBasedDatasource({ LayerSettingsComponent(props) { return <LayerSettingsPanel {...props} />; }, - DataPanelComponent(props: DatasourceDataPanelProps<FormBasedPrivateState>) { + DataPanelComponent(props: DatasourceDataPanelProps<FormBasedPrivateState, Query>) { const { onChangeIndexPattern, ...otherProps } = props; const layerFields = formBasedDatasource?.getSelectedFields?.(props.state); return ( @@ -869,13 +869,11 @@ export function getFormBasedDatasource({ getDatasourceInfo: async (state, references, dataViewsService) => { const layers = references ? injectReferences(state, references).layers : state.layers; - const indexPatterns: DataView[] = []; - for (const { indexPatternId } of Object.values(layers)) { - const dataView = await dataViewsService?.get(indexPatternId); - if (dataView) { - indexPatterns.push(dataView); - } - } + const indexPatterns: DataView[] = await Promise.all( + Object.values(layers) + .map(({ indexPatternId }) => dataViewsService?.get(indexPatternId)) + .filter(nonNullable) + ); return Object.entries(layers).reduce<DataSourceInfo[]>((acc, [key, layer]) => { const dataView = indexPatterns?.find( (indexPattern) => indexPattern.id === layer.indexPatternId diff --git a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts index fcefa97ecd4b1..f98107eebbcca 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/mocks.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/mocks.ts @@ -8,101 +8,83 @@ import { getFieldByNameFactory } from './pure_helpers'; import type { IndexPattern, IndexPatternField } from '../../types'; +export function createMockedField( + someProps: Partial<IndexPatternField> & Pick<IndexPatternField, 'name' | 'type'> +) { + return { + displayName: someProps.name, + aggregatable: true, + searchable: true, + ...someProps, + }; +} + export const createMockedIndexPattern = ( someProps?: Partial<IndexPattern>, customFields: IndexPatternField[] = [] ): IndexPattern => { const fields = [ - { + createMockedField({ name: 'timestamp', displayName: 'timestampLabel', type: 'date', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'start_date', - displayName: 'start_date', type: 'date', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'bytes', - displayName: 'bytes', type: 'number', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'memory', - displayName: 'memory', type: 'number', - aggregatable: true, - searchable: true, esTypes: ['float'], - }, - { + }), + createMockedField({ name: 'source', - displayName: 'source', type: 'string', - aggregatable: true, - searchable: true, esTypes: ['keyword'], - }, - { + }), + createMockedField({ name: 'unsupported', - displayName: 'unsupported', type: 'geo', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'dest', - displayName: 'dest', type: 'string', - aggregatable: true, - searchable: true, esTypes: ['keyword'], - }, - { + }), + createMockedField({ name: 'geo.src', - displayName: 'geo.src', type: 'string', - aggregatable: true, - searchable: true, esTypes: ['keyword'], - }, - { + }), + createMockedField({ name: 'scripted', displayName: 'Scripted', type: 'string', - searchable: true, - aggregatable: true, scripted: true, lang: 'painless' as const, script: '1234', - }, - { + }), + createMockedField({ name: 'runtime-keyword', displayName: 'Runtime keyword field', type: 'string', - searchable: true, - aggregatable: true, runtime: true, lang: 'painless' as const, script: 'emit("123")', - }, - { + }), + createMockedField({ name: 'runtime-number', displayName: 'Runtime number field', type: 'number', - searchable: true, - aggregatable: true, runtime: true, lang: 'painless' as const, script: 'emit(123)', - }, + }), ...(customFields || []), ]; return { @@ -120,31 +102,23 @@ export const createMockedIndexPattern = ( export const createMockedRestrictedIndexPattern = () => { const fields = [ - { + createMockedField({ name: 'timestamp', displayName: 'timestampLabel', type: 'date', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'bytes', - displayName: 'bytes', type: 'number', - aggregatable: true, - searchable: true, - }, - { + }), + createMockedField({ name: 'source', - displayName: 'source', type: 'string', - aggregatable: true, - searchable: true, scripted: true, esTypes: ['keyword'], lang: 'painless' as const, script: '1234', - }, + }), ]; return { id: '2', diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index 411583d88ef13..6a9471e174e80 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -362,7 +362,7 @@ export function getTextBasedDatasource({ getUsedDataViews: (state) => { return Object.values(state.layers) .map(({ index }) => index) - .filter((index) => index !== undefined) as string[]; + .filter(nonNullable); }, getPersistableState({ layers }: TextBasedPrivateState) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/easteregg/index.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/easteregg/index.tsx index 7bfd7c666079a..3372625ff2830 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/easteregg/index.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/easteregg/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import type { Query } from '@kbn/es-query'; +import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; import { EuiErrorBoundary } from '@elastic/eui'; const Bee = React.lazy(() => import('./bee')); @@ -34,11 +34,14 @@ function Bees({ query }: { query?: Query }) { ); } -export function Easteregg(props: { query?: Query }) { +export function Easteregg(props: { query?: Query | AggregateQuery }) { + if (isOfAggregateQueryType(props.query)) { + return null; + } return ( // Do not break Lens for an easteregg <EuiErrorBoundary style={{ display: 'none' }}> - <Bees {...props} /> + <Bees query={props.query} /> </EuiErrorBoundary> ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 466773ec1c6b2..efe3ccc84f560 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -33,7 +33,7 @@ import type { SuggestionRequest, } from '../../types'; import { buildExpression } from './expression_helpers'; -import { Document } from '../../persistence/saved_object_store'; +import { LensDocument } from '../../persistence/saved_object_store'; import { getActiveDatasourceIdFromDoc, sortDataViewRefs } from '../../utils'; import type { DatasourceState, DatasourceStates, VisualizationState } from '../../state_management'; import { readFromStorage } from '../../settings_storage'; @@ -353,12 +353,13 @@ export interface DocumentToExpressionReturnType { indexPatterns: IndexPatternMap; indexPatternRefs: IndexPatternRef[]; activeVisualizationState: unknown; + activeDatasourceState: unknown; } export async function persistedStateToExpression( datasourceMap: DatasourceMap, visualizations: VisualizationMap, - doc: Document, + doc: LensDocument, services: { uiSettings: IUiSettingsClient; storage: IStorageWrapper; @@ -381,7 +382,13 @@ export async function persistedStateToExpression( description, } = doc; if (!visualizationType) { - return { ast: null, indexPatterns: {}, indexPatternRefs: [], activeVisualizationState: null }; + return { + ast: null, + indexPatterns: {}, + indexPatternRefs: [], + activeVisualizationState: null, + activeDatasourceState: null, + }; } const annotationGroups = await initializeEventAnnotationGroups( @@ -435,6 +442,7 @@ export async function persistedStateToExpression( indexPatterns, indexPatternRefs, activeVisualizationState, + activeDatasourceState: null, }; } @@ -454,6 +462,7 @@ export async function persistedStateToExpression( nowInstant: services.nowProvider.get(), }), activeVisualizationState, + activeDatasourceState: datasourceStates[datasourceId]?.state, indexPatterns, indexPatternRefs, }; 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 b32d7456bd2b5..5775748da8cee 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 @@ -248,7 +248,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const removeExpressionBuildErrorsRef = useRef<() => void>(); const onData$ = useCallback( - (_data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => { + (_data: unknown, adapters?: DefaultInspectorAdapters) => { if (renderDeps.current) { dataReceivedTime.current = performance.now(); @@ -283,10 +283,11 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ dispatchLens( onActiveDataChange({ activeData: Object.entries(adapters.tables?.tables).reduce<Record<string, Datatable>>( - (acc, [key, value], _index, tables) => ({ - ...acc, - [tables.length === 1 ? defaultLayerId : key]: value, - }), + (acc, [key, value], _index, tables) => { + const id = tables.length === 1 ? defaultLayerId : key; + acc[id] = value as Datatable; + return acc; + }, {} ), }) @@ -726,7 +727,7 @@ export const VisualizationWrapper = ({ ExpressionRendererComponent: ReactExpressionRendererType; core: CoreStart; onRender$: () => void; - onData$: (data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => void; + onData$: (data: unknown, adapters?: DefaultInspectorAdapters) => void; onComponentRendered: () => void; displayOptions: VisualizationDisplayOptions | undefined; }) => { @@ -788,7 +789,7 @@ export const VisualizationWrapper = ({ // @ts-expect-error upgrade typescript v4.9.5 onData$={onData$} onRender$={onRenderHandler} - inspectorAdapters={lensInspector.adapters} + inspectorAdapters={lensInspector.getInspectorAdapters()} executionContext={executionContext} renderMode="edit" renderError={(errorMessage?: string | null, error?: ExpressionRenderError | null) => { 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 71cf62d02d388..a677e0c6105b8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -24,7 +24,7 @@ import { DataViewsPublicPluginStart, } from '@kbn/data-views-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; -import { Document } from '../persistence/saved_object_store'; +import { LensDocument } from '../persistence/saved_object_store'; import { Datasource, Visualization, @@ -93,7 +93,7 @@ export class EditorFrameService { * This is an asynchronous process. * @param doc parsed Lens saved object */ - public documentToExpression = async (doc: Document, services: EditorFramePlugins) => { + public documentToExpression = async (doc: LensDocument, services: EditorFramePlugins) => { const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ this.loadDatasources(), this.loadVisualizations(), diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx deleted file mode 100644 index 3dda0daf25760..0000000000000 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ /dev/null @@ -1,1373 +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 { - Embeddable, - LensByValueInput, - LensUnwrapMetaInfo, - LensEmbeddableInput, - LensByReferenceInput, - LensSavedObjectAttributes, - LensUnwrapResult, - LensEmbeddableDeps, -} from './embeddable'; -import { ReactExpressionRendererProps } from '@kbn/expressions-plugin/public'; -import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; -import { Filter, Query, TimeRange } from '@kbn/es-query'; -import { FilterManager } from '@kbn/data-plugin/public'; -import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { Document } from '../persistence'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public/embeddable'; -import { coreMock, httpServiceMock } from '@kbn/core/public/mocks'; -import { IBasePath, IUiSettingsClient } from '@kbn/core/public'; -import { AttributeService, ViewMode } from '@kbn/embeddable-plugin/public'; -import { LensAttributeService } from '../lens_attribute_service'; -import { OnSaveProps } from '@kbn/saved-objects-plugin/public/save_modal'; -import { act } from 'react-dom/test-utils'; -import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; -import { Visualization } from '../types'; -import { createMockDatasource, createMockVisualization } from '../mocks'; -import { FIELD_NOT_FOUND, FIELD_WRONG_TYPE } from '../user_messages_ids'; - -jest.mock('@kbn/inspector-plugin/public', () => ({ - isAvailable: false, - open: false, -})); - -const defaultVisualizationId = 'lnsSomeVisType'; -const defaultDatasourceId = 'someDatasource'; - -const savedVis: Document = { - state: { - visualization: { activeId: defaultVisualizationId }, - datasourceStates: { [defaultDatasourceId]: {} }, - query: { query: '', language: 'lucene' }, - filters: [], - }, - references: [], - title: 'My title', - visualizationType: defaultVisualizationId, -}; - -const defaultVisualizationMap = { - [defaultVisualizationId]: createMockVisualization(), -}; - -const defaultDatasourceMap = { - [defaultDatasourceId]: createMockDatasource(defaultDatasourceId), -}; - -const defaultSaveMethod = ( - _testAttributes: LensSavedObjectAttributes, - _savedObjectId?: string -): Promise<{ id: string }> => { - return new Promise(() => { - return { id: '123' }; - }); -}; -const defaultUnwrapMethod = ( - _savedObjectId: string -): Promise<{ attributes: LensSavedObjectAttributes }> => { - return new Promise(() => { - return { attributes: { ...savedVis } }; - }); -}; -const defaultCheckForDuplicateTitle = (_props: OnSaveProps): Promise<true> => { - return new Promise(() => { - return true; - }); -}; -const options = { - saveMethod: defaultSaveMethod, - unwrapMethod: defaultUnwrapMethod, - checkForDuplicateTitle: defaultCheckForDuplicateTitle, -}; - -const mockInjectFilterReferences: FilterManager['inject'] = (filters, _references) => { - return filters.map((filter) => ({ - ...filter, - meta: { - ...filter.meta, - index: 'injected!', - }, - })); -}; - -const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => { - const core = coreMock.createStart(); - const service = new AttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput, - LensUnwrapMetaInfo - >('lens', core.notifications.toasts, options); - service.unwrapAttributes = jest.fn((_input: LensByValueInput | LensByReferenceInput) => { - return Promise.resolve({ - attributes: { - ...document, - }, - metaInfo: { - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, - }, - } as LensUnwrapResult); - }); - service.wrapAttributes = jest.fn(); - return service; -}; - -const dataMock = dataPluginMock.createStartContract(); - -describe('embeddable', () => { - const coreStart = coreMock.createStart(); - - let mountpoint: HTMLDivElement; - let expressionRenderer: jest.Mock<null, [ReactExpressionRendererProps]>; - let getTrigger: jest.Mock; - let trigger: { exec: jest.Mock }; - let basePath: IBasePath; - let attributeService: AttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput, - LensUnwrapMetaInfo - >; - - beforeEach(() => { - mountpoint = document.createElement('div'); - expressionRenderer = jest.fn((_props) => null); - trigger = { exec: jest.fn() }; - getTrigger = jest.fn(() => trigger); - attributeService = attributeServiceMockFromSavedVis(savedVis); - const http = httpServiceMock.createSetupContract({ basePath: '/test' }); - basePath = http.basePath; - }); - - afterEach(() => { - mountpoint.remove(); - }); - - function getEmbeddableProps(props: Partial<LensEmbeddableDeps> = {}): LensEmbeddableDeps { - return { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - inspector: inspectorPluginMock.createStartContract(), - expressionRenderer, - coreStart, - basePath, - dataViews: { - get: (id: string) => Promise.resolve({ id, isTimeBased: () => false }), - } as unknown as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - ...props, - }; - } - - it('should render expression once with expression renderer', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(`my -| expression`); - }); - - it('should not throw if render is called after destroy', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput); - let renderCalled = false; - let renderThrew = false; - // destroying completes output synchronously which might make a synchronous render call - this shouldn't throw - embeddable.getOutput$().subscribe(undefined, undefined, () => { - try { - embeddable.render(mountpoint); - } catch (e) { - renderThrew = true; - } finally { - renderCalled = true; - } - }); - embeddable.destroy(); - expect(renderCalled).toBe(true); - expect(renderThrew).toBe(false); - }); - - it('should render once even if reload is called before embeddable is fully initialized', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput); - embeddable.reload(); - expect(expressionRenderer).toHaveBeenCalledTimes(0); - embeddable.render(mountpoint); - expect(expressionRenderer).toHaveBeenCalledTimes(0); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - }); - - it('should not render the visualization if any error arises', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), {} as LensEmbeddableInput); - - jest.spyOn(embeddable, 'getUserMessages').mockReturnValue([ - { - uniqueId: 'error', - severity: 'error', - fixableInEditor: true, - displayLocations: [{ id: 'visualization' }], - longMessage: 'lol', - shortMessage: 'lol', - }, - ]); - - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(0); - }); - - it('should override embeddableBadge message', async () => { - const getBadgeMessage = jest.fn( - (): ReturnType<NonNullable<LensEmbeddableInput['onBeforeBadgesRender']>> => [ - { - uniqueId: FIELD_NOT_FOUND, - severity: 'warning', - fixableInEditor: true, - displayLocations: [ - { id: 'embeddableBadge' }, - { id: 'dimensionButton', dimensionId: '1' }, - ], - longMessage: 'custom', - shortMessage: '', - hidePopoverIcon: true, - }, - ] - ); - - const embeddable = new Embeddable( - getEmbeddableProps({ - datasourceMap: { - ...defaultDatasourceMap, - [defaultDatasourceId]: { - ...defaultDatasourceMap[defaultDatasourceId], - getUserMessages: jest.fn(() => [ - { - uniqueId: FIELD_NOT_FOUND, - severity: 'error', - fixableInEditor: true, - displayLocations: [ - { id: 'embeddableBadge' }, - { id: 'dimensionButton', dimensionId: '1' }, - ], - longMessage: 'original', - shortMessage: '', - }, - { - uniqueId: FIELD_WRONG_TYPE, - severity: 'error', - fixableInEditor: true, - displayLocations: [{ id: 'visualization' }], - longMessage: 'original', - shortMessage: '', - }, - ]), - }, - }, - }), - { - onBeforeBadgesRender: getBadgeMessage as LensEmbeddableInput['onBeforeBadgesRender'], - } as LensEmbeddableInput - ); - - const getUserMessagesSpy = jest.spyOn(embeddable, 'getUserMessages'); - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - - embeddable.render(mountpoint); - - expect(getUserMessagesSpy.mock.results.flatMap((r) => r.value)).toEqual( - expect.arrayContaining([ - { - uniqueId: FIELD_WRONG_TYPE, - severity: 'error', - fixableInEditor: true, - displayLocations: [{ id: 'visualization' }], - longMessage: 'original', - shortMessage: '', - }, - { - uniqueId: FIELD_NOT_FOUND, - severity: 'warning', - fixableInEditor: true, - displayLocations: [ - { id: 'embeddableBadge' }, - { id: 'dimensionButton', dimensionId: '1' }, - ], - longMessage: 'custom', - shortMessage: '', - hidePopoverIcon: true, - }, - ]) - ); - }); - - it('should not render the vis if loaded saved object conflicts', async () => { - attributeService.unwrapAttributes = jest.fn( - (_input: LensByValueInput | LensByReferenceInput) => { - return Promise.resolve({ - attributes: { - ...savedVis, - }, - metaInfo: { - sharingSavedObjectProps: { - outcome: 'conflict', - sourceId: '1', - aliasTargetId: '2', - }, - }, - } as LensUnwrapResult); - } - ); - const spacesPluginStart = spacesPluginMock.createStartContract(); - spacesPluginStart.ui.components.getEmbeddableLegacyUrlConflict = jest.fn(() => ( - <>getEmbeddableLegacyUrlConflict</> - )); - const embeddable = new Embeddable( - getEmbeddableProps({ - spaces: spacesPluginStart, - attributeService, - }), - {} as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - embeddable.render(mountpoint); - expect(expressionRenderer).toHaveBeenCalledTimes(0); - expect(spacesPluginStart.ui.components.getEmbeddableLegacyUrlConflict).toHaveBeenCalled(); - }); - - it('should not render if timeRange prop is not passed when a referenced data view is time based', async () => { - const embeddable = new Embeddable( - getEmbeddableProps({ - attributeService: attributeServiceMockFromSavedVis({ - ...savedVis, - references: [ - { type: 'index-pattern', id: '123', name: 'abc' }, - { type: 'index-pattern', id: '123', name: 'def' }, - { type: 'index-pattern', id: '456', name: 'ghi' }, - ], - }), - dataViews: { - get: (id: string) => Promise.resolve({ id, isTimeBased: () => true }), - } as unknown as DataViewsContract, - }), - {} as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - embeddable.render(mountpoint); - expect(expressionRenderer).toHaveBeenCalledTimes(0); - }); - - it('should initialize output with deduped list of index patterns', async () => { - const embeddable = new Embeddable( - getEmbeddableProps({ - attributeService: attributeServiceMockFromSavedVis({ - ...savedVis, - references: [ - { type: 'index-pattern', id: '123', name: 'abc' }, - { type: 'index-pattern', id: '123', name: 'def' }, - { type: 'index-pattern', id: '456', name: 'ghi' }, - ], - }), - }), - {} as LensEmbeddableInput - ); - - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - const outputIndexPatterns = embeddable.getOutput().indexPatterns!; - expect(outputIndexPatterns.length).toEqual(2); - expect(outputIndexPatterns[0].id).toEqual('123'); - expect(outputIndexPatterns[1].id).toEqual('456'); - }); - - it('should re-render once on filter change', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - }, - { id: '123' } as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - filters: [{ meta: { alias: 'test', negate: false, disabled: false } }], - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should re-render once on search session change', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - }, - { id: '123', searchSessionId: 'firstSession' } as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - searchSessionId: 'nextSession', - }); - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => { - const sampleInput = { - id: '123', - enhancements: { - dynamicActions: {}, - }, - } as unknown as LensEmbeddableInput; - const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - viewMode: ViewMode.VIEW, - }); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - ...sampleInput, - viewMode: ViewMode.VIEW, - }); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should re-render when dynamic actions input changes', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - enhancements: { - dynamicActions: {}, - }, - }); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should pass context to embeddable', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; - - const input = { - savedObjectId: '123', - timeRange, - query, - filters, - searchSessionId: 'searchSessionId', - } as LensEmbeddableInput; - - const embeddable = new Embeddable(getEmbeddableProps(), input); - await embeddable.initializeSavedVis(input); - embeddable.render(mountpoint); - - expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual( - expect.objectContaining({ - timeRange, - query: [query, savedVis.state.query], - filters, - }) - ); - - expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId); - }); - - it('should pass render mode to expression', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; - - const input = { - savedObjectId: '123', - timeRange, - query, - filters, - renderMode: 'view', - disableTriggers: true, - } as LensEmbeddableInput; - - const embeddable = new Embeddable(getEmbeddableProps(), input); - await embeddable.initializeSavedVis(input); - embeddable.render(mountpoint); - - expect(expressionRenderer.mock.calls[0][0]).toEqual( - expect.objectContaining({ - interactive: false, - renderMode: 'view', - }) - ); - }); - - it('should merge external context with query and filters of the saved object', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: 'external query' }; - const filters: Filter[] = [ - { meta: { alias: 'external filter', negate: false, disabled: false } }, - ]; - - const newSavedVis = { - ...savedVis, - state: { - ...savedVis.state, - query: { language: 'kquery', query: 'saved filter' }, - filters: [{ meta: { alias: 'test', negate: false, disabled: false, index: 'filter-0' } }], - }, - references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], - }; - - const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; - - const embeddable = new Embeddable( - getEmbeddableProps({ attributeService: attributeServiceMockFromSavedVis(newSavedVis) }), - input - ); - await embeddable.initializeSavedVis(input); - embeddable.render(mountpoint); - - const expectedFilters = [ - ...input.filters!, - ...mockInjectFilterReferences(newSavedVis.state.filters, []), - ]; - expect(expressionRenderer.mock.calls[0][0].searchContext?.timeRange).toEqual(timeRange); - expect(expressionRenderer.mock.calls[0][0].searchContext?.filters).toEqual(expectedFilters); - expect(expressionRenderer.mock.calls[0][0].searchContext?.query).toEqual([ - query, - { language: 'kquery', query: 'saved filter' }, - ]); - }); - - it('should execute trigger on event from expression renderer', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; - - const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; - onEvent({ name: 'brush', data: eventData }); - - expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); - expect(trigger.exec).toHaveBeenCalledWith( - expect.objectContaining({ - data: { ...eventData, timeFieldName: undefined }, - embeddable: expect.anything(), - }) - ); - }); - - it('should execute trigger on row click event from expression renderer', async () => { - const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; - - onEvent({ name: 'tableRowContextMenuClick', data: {} }); - - expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick); - }); - - it('should not re-render if only change is in disabled filter', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; - - const embeddable = new Embeddable(getEmbeddableProps(), { - id: '123', - timeRange, - query, - filters, - } as LensEmbeddableInput); - await embeddable.initializeSavedVis({ - id: '123', - timeRange, - query, - filters, - } as LensEmbeddableInput); - embeddable.render(mountpoint); - - act(() => { - embeddable.updateInput({ - timeRange, - query, - filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], - }); - }); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - }); - - it('should call onload after rerender and onData$ call ', async () => { - const onDataTimeout = 10; - const onLoad = jest.fn(); - const adapters = { tables: {} }; - - expressionRenderer = jest.fn(({ onData$ }) => { - setTimeout(() => { - onData$?.({}, adapters); - }, onDataTimeout); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onLoad, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(onLoad).toHaveBeenCalledWith(true); - expect(onLoad).toHaveBeenCalledTimes(1); - - await new Promise((resolve) => setTimeout(resolve, onDataTimeout * 1.5)); - - // loading should become false - expect(onLoad).toHaveBeenCalledTimes(2); - expect(onLoad).toHaveBeenNthCalledWith(2, false, adapters, embeddable.getOutput$()); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - searchSessionId: 'newSession', - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - // loading should become again true - expect(onLoad).toHaveBeenCalledTimes(3); - expect(onLoad).toHaveBeenNthCalledWith(3, true); - expect(expressionRenderer).toHaveBeenCalledTimes(2); - - await new Promise((resolve) => setTimeout(resolve, onDataTimeout * 1.5)); - - // loading should again become false - expect(onLoad).toHaveBeenCalledTimes(4); - expect(onLoad).toHaveBeenNthCalledWith(4, false, adapters, embeddable.getOutput$()); - }); - - it('should call onFilter event on filter call ', async () => { - const onFilter = jest.fn(); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ - name: 'filter', - data: { pings: false, table: { rows: [], columns: [] }, column: 0 }, - }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onFilter, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(onFilter).toHaveBeenCalledWith(expect.objectContaining({ pings: false })); - expect(onFilter).toHaveBeenCalledTimes(1); - }); - - it('should prevent the onFilter trigger when calling preventDefault', async () => { - const onFilter = jest.fn(({ preventDefault }) => preventDefault()); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ - name: 'filter', - data: { pings: false, table: { rows: [], columns: [] }, column: 0 }, - }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onFilter, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(getTrigger).not.toHaveBeenCalled(); - }); - - it('should call onBrush event on brushing', async () => { - const onBrushEnd = jest.fn(); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ - name: 'brush', - data: { range: [0, 1], table: { rows: [], columns: [] }, column: 0 }, - }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onBrushEnd, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(onBrushEnd).toHaveBeenCalledWith(expect.objectContaining({ range: [0, 1] })); - expect(onBrushEnd).toHaveBeenCalledTimes(1); - }); - - it('should prevent the onBrush trigger when calling preventDefault', async () => { - const onBrushEnd = jest.fn(({ preventDefault }) => preventDefault()); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ - name: 'brush', - data: { range: [0, 1], table: { rows: [], columns: [] }, column: 0 }, - }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onBrushEnd, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(getTrigger).not.toHaveBeenCalled(); - }); - - it('should call onTableRowClick event ', async () => { - const onTableRowClick = jest.fn(); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ name: 'tableRowContextMenuClick', data: { name: 'test' } }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onTableRowClick, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(onTableRowClick).toHaveBeenCalledWith(expect.objectContaining({ name: 'test' })); - expect(onTableRowClick).toHaveBeenCalledTimes(1); - }); - - it('should prevent onTableRowClick trigger when calling preventDefault ', async () => { - const onTableRowClick = jest.fn(({ preventDefault }) => preventDefault()); - - expressionRenderer = jest.fn(({ onEvent }) => { - setTimeout(() => { - onEvent?.({ name: 'tableRowContextMenuClick', data: { name: 'test' } }); - }, 10); - - return null; - }); - - const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { - id: '123', - onTableRowClick, - } as unknown as LensEmbeddableInput); - - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 20)); - - expect(getTrigger).not.toHaveBeenCalled(); - }); - - it('handles edit actions ', async () => { - const editedVisualizationState = { value: 'edited' }; - const onEditActionMock = jest.fn().mockReturnValue(editedVisualizationState); - const documentToExpressionMock = jest.fn().mockImplementation(async (document) => { - const isStateEdited = document.state.visualization.value === 'edited'; - return { - ast: { - type: 'expression', - chain: [ - { - type: 'function', - function: isStateEdited ? 'edited' : 'not_edited', - arguments: {}, - }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }; - }); - - const visDocument: Document = { - state: { - visualization: {}, - datasourceStates: { [defaultDatasourceId]: {} }, - query: { query: '', language: 'lucene' }, - filters: [], - }, - references: [], - title: 'My title', - visualizationType: 'lensDatatable', - }; - - const embeddable = new Embeddable( - getEmbeddableProps({ - attributeService: attributeServiceMockFromSavedVis(visDocument), - visualizationMap: { - [visDocument.visualizationType as string]: { - onEditAction: onEditActionMock, - initialize: () => {}, - } as unknown as Visualization, - }, - documentToExpression: documentToExpressionMock, - }), - { id: '123' } as unknown as LensEmbeddableInput - ); - - // SETUP FRESH STATE - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.expression).toBe(`not_edited`); - - // TEST EDIT EVENT - await embeddable.handleEvent({ name: 'edit' }); - - expect(onEditActionMock).toHaveBeenCalledTimes(1); - expect(documentToExpressionMock).toHaveBeenCalled(); - - const docToExpCalls = documentToExpressionMock.mock.calls; - const editedVisDocument = docToExpCalls[docToExpCalls.length - 1][0]; - expect(editedVisDocument.state.visualization).toEqual(editedVisualizationState); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - expect(expressionRenderer.mock.calls[1][0]!.expression).toBe(`edited`); - }); - - it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { - expressionRenderer = jest.fn((_) => null); - - const createEmbeddable = (displayOptions?: { noPadding: boolean }, noPadding?: boolean) => { - return new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService: attributeServiceMockFromSavedVis(savedVis), - data: dataMock, - expressionRenderer, - coreStart, - basePath, - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - inspector: inspectorPluginMock.createStartContract(), - getTrigger, - visualizationMap: { - [savedVis.visualizationType as string]: { - getDisplayOptions: displayOptions ? () => displayOptions : undefined, - initialize: () => {}, - } as unknown as Visualization, - }, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - }, - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - noPadding, - } as LensEmbeddableInput - ); - }; - - // no display options and no override - let embeddable = createEmbeddable(); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.padding).toBe('s'); - - // display options and no override - embeddable = createEmbeddable({ noPadding: true }); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); - - // no display options and override - embeddable = createEmbeddable(undefined, true); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(3); - expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); - - // display options and override - embeddable = createEmbeddable({ noPadding: false }, true); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(4); - expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); - }); - - it('should reload only once when the attributes or savedObjectId and the search context change at the same time', async () => { - const createEmbeddable = async () => { - const currentExpressionRenderer = jest.fn((_props) => null); - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer: currentExpressionRenderer, - coreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - }, - { id: '123', timeRange, query, filters } as LensEmbeddableInput - ); - const reload = jest.spyOn(embeddable, 'reload'); - const initializeSavedVis = jest.spyOn(embeddable, 'initializeSavedVis'); - - await embeddable.initializeSavedVis({ - id: '123', - timeRange, - query, - filters, - } as LensEmbeddableInput); - - embeddable.render(mountpoint); - - return { - embeddable, - reload, - initializeSavedVis, - expressionRenderer: currentExpressionRenderer, - }; - }; - - let test = await createEmbeddable(); - - expect(test.reload).toHaveBeenCalledTimes(1); - expect(test.initializeSavedVis).toHaveBeenCalledTimes(1); - expect(test.expressionRenderer).toHaveBeenCalledTimes(1); - - // Test with savedObjectId and searchSessionId change - act(() => { - test.embeddable.updateInput({ savedObjectId: '123', searchSessionId: '456' }); - }); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(test.reload).toHaveBeenCalledTimes(2); - expect(test.initializeSavedVis).toHaveBeenCalledTimes(2); - expect(test.expressionRenderer).toHaveBeenCalledTimes(2); - - test = await createEmbeddable(); - - expect(test.reload).toHaveBeenCalledTimes(1); - expect(test.initializeSavedVis).toHaveBeenCalledTimes(1); - expect(test.expressionRenderer).toHaveBeenCalledTimes(1); - - // Test with attributes and timeRange change - act(() => { - test.embeddable.updateInput({ - attributes: { foo: 'bar' } as unknown as LensSavedObjectAttributes, - timeRange: { from: 'now-30d', to: 'now' }, - }); - }); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(test.reload).toHaveBeenCalledTimes(2); - expect(test.initializeSavedVis).toHaveBeenCalledTimes(2); - expect(test.expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should get full attributes', async () => { - const createEmbeddable = async () => { - const currentExpressionRenderer = jest.fn((_props) => null); - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer: currentExpressionRenderer, - coreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - }, - { id: '123', timeRange, query, filters } as LensEmbeddableInput - ); - const reload = jest.spyOn(embeddable, 'reload'); - const initializeSavedVis = jest.spyOn(embeddable, 'initializeSavedVis'); - - await embeddable.initializeSavedVis({ - id: '123', - timeRange, - query, - filters, - } as LensEmbeddableInput); - - embeddable.render(mountpoint); - - return { - embeddable, - reload, - initializeSavedVis, - expressionRenderer: currentExpressionRenderer, - }; - }; - - const test = await createEmbeddable(); - - expect(test.embeddable.getFullAttributes()).toEqual(savedVis); - }); - - it('should pass over the overrides as variables', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - expressionRenderer, - coreStart, - basePath, - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - canOpenVisualizations: true, - discover: {}, - navLinks: {}, - }, - inspector: inspectorPluginMock.createStartContract(), - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: null, - }), - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - }, - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - overrides: { - settings: { - onBrushEnd: 'ignore', - }, - }, - } as LensEmbeddableInput - ); - embeddable.render(mountpoint); - - // wait one tick to give embeddable time to initialize - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.variables).toEqual( - expect.objectContaining({ - overrides: { - settings: { - onBrushEnd: 'ignore', - }, - }, - }) - ); - }); - - it('should not be editable for no visualize library privileges', async () => { - const embeddable = new Embeddable( - getEmbeddableProps({ - capabilities: { - canSaveDashboards: false, - canSaveVisualizations: true, - canOpenVisualizations: false, - discover: {}, - navLinks: {}, - }, - }), - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput - ); - expect(embeddable.getOutput().editable).toBeUndefined(); - }); -}); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx deleted file mode 100644 index ce86b896d5fa0..0000000000000 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ /dev/null @@ -1,1719 +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 { partition, uniqBy } from 'lodash'; -import React from 'react'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; -import { - DataViewBase, - EsQueryConfig, - Filter, - Query, - AggregateQuery, - TimeRange, - isOfQueryType, - getAggregateQueryMode, - ExecutionContextSearch, - getLanguageDisplayName, - isOfAggregateQueryType, -} from '@kbn/es-query'; -import type { PaletteOutput } from '@kbn/coloring'; -import { - DataPublicPluginStart, - TimefilterContract, - FilterManager, - getEsQueryConfig, - mapAndFlattenFilters, -} from '@kbn/data-plugin/public'; -import type { Start as InspectorStart } from '@kbn/inspector-plugin/public'; - -import { merge, Subscription, switchMap } from 'rxjs'; -import { toExpression } from '@kbn/interpreter'; -import { - Datatable, - DefaultInspectorAdapters, - ErrorLike, - RenderMode, -} from '@kbn/expressions-plugin/common'; -import { map, distinctUntilChanged, skip, debounceTime } from 'rxjs'; -import fastIsEqual from 'fast-deep-equal'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { - ExpressionRendererEvent, - ReactExpressionRendererType, -} from '@kbn/expressions-plugin/public'; -import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; - -import { - EmbeddableStateTransfer, - Embeddable as AbstractEmbeddable, - EmbeddableInput, - EmbeddableOutput, - IContainer, - SavedObjectEmbeddableInput, - ReferenceOrValueEmbeddable, - SelfStyledEmbeddable, - FilterableEmbeddable, - cellValueTrigger, - CELL_VALUE_TRIGGER, - type CellValueContext, - shouldFetch$, -} from '@kbn/embeddable-plugin/public'; -import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; -import type { - Capabilities, - CoreStart, - IBasePath, - IUiSettingsClient, - KibanaExecutionContext, -} from '@kbn/core/public'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { - BrushTriggerEvent, - ClickTriggerEvent, - MultiClickTriggerEvent, -} from '@kbn/charts-plugin/public'; -import { DataViewSpec } from '@kbn/data-views-plugin/common'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useEuiFontSize, useEuiTheme, EuiEmptyPrompt } from '@elastic/eui'; -import { canTrackContentfulRender } from '@kbn/presentation-containers'; -import { getSuccessfulRequestTimings } from '../report_performance_metric_util'; -import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry'; -import { Document } from '../persistence'; -import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'; -import { - isLensBrushEvent, - isLensFilterEvent, - isLensMultiFilterEvent, - isLensEditEvent, - isLensTableRowContextMenuClickEvent, - LensTableRowContextMenuEvent, - VisualizationMap, - Visualization, - DatasourceMap, - Datasource, - IndexPatternMap, - GetCompatibleCellValueActions, - UserMessage, - IndexPatternRef, - FramePublicAPI, - AddUserMessages, - UserMessagesGetter, - UserMessagesDisplayLocationId, -} from '../types'; - -import type { - AllowedChartOverrides, - AllowedPartitionOverrides, - AllowedSettingsOverrides, - AllowedGaugeOverrides, - AllowedXYOverrides, -} from '../../common/types'; -import { getEditPath, DOC_TYPE, APP_ID } from '../../common/constants'; -import { LensAttributeService } from '../lens_attribute_service'; -import type { TableInspectorAdapter } from '../editor_frame_service/types'; -import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; -import { SharingSavedObjectProps, VisualizationDisplayOptions } from '../types'; -import { - getActiveDatasourceIdFromDoc, - getActiveVisualizationIdFromDoc, - getIndexPatternsObjects, - getSearchWarningMessages, - inferTimeField, - extractReferencesFromState, -} from '../utils'; -import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data'; -import { - filterAndSortUserMessages, - getApplicationUserMessages, -} from '../app_plugin/get_application_user_messages'; -import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list'; -import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame'; -import type { TypedLensByValueInput } from './embeddable_component'; -import type { LensPluginStartDependencies } from '../plugin'; -import { EmbeddableFeatureBadge } from './embeddable_info_badges'; -import { getDatasourceLayers } from '../state_management/utils'; -import type { EditLensConfigurationProps } from '../app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration'; -import { TextBasedPersistedState } from '../datasources/text_based/types'; -import { getLongMessage } from '../user_messages_utils'; - -export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>; - -export interface LensUnwrapMetaInfo { - sharingSavedObjectProps?: SharingSavedObjectProps; - managed?: boolean; -} - -export interface LensUnwrapResult { - attributes: LensSavedObjectAttributes; - metaInfo?: LensUnwrapMetaInfo; -} - -interface PreventableEvent { - preventDefault(): void; -} - -export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {}; - -export interface LensBaseEmbeddableInput extends EmbeddableInput { - filters?: Filter[]; - query?: Query; - timeRange?: TimeRange; - timeslice?: [number, number]; - palette?: PaletteOutput; - renderMode?: RenderMode; - style?: React.CSSProperties; - className?: string; - noPadding?: boolean; - onBrushEnd?: (data: Simplify<BrushTriggerEvent['data'] & PreventableEvent>) => void; - onLoad?: ( - isLoading: boolean, - adapters?: Partial<DefaultInspectorAdapters>, - output$?: Observable<LensEmbeddableOutput> - ) => void; - onFilter?: ( - data: Simplify<(ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) & PreventableEvent> - ) => void; - onTableRowClick?: ( - data: Simplify<LensTableRowContextMenuEvent['data'] & PreventableEvent> - ) => void; - abortController?: AbortController; - onBeforeBadgesRender?: (userMessages: UserMessage[]) => UserMessage[]; -} - -export type LensByValueInput = { - attributes: LensSavedObjectAttributes; - /** - * Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline. - * Each visualization type offers various type of overrides, per component (i.e. 'setting', 'axisX', 'partition', etc...) - * - * While it is not possible to pass function/callback/handlers to the renderer, it is possible to overwrite - * the current behaviour by passing the "ignore" string to the override prop (i.e. onBrushEnd: "ignore" to stop brushing) - */ - overrides?: - | AllowedChartOverrides - | AllowedSettingsOverrides - | AllowedXYOverrides - | AllowedPartitionOverrides - | AllowedGaugeOverrides; -} & LensBaseEmbeddableInput; - -export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput; -export type LensEmbeddableInput = LensByValueInput | LensByReferenceInput; - -export interface LensEmbeddableOutput extends EmbeddableOutput { - indexPatterns?: DataView[]; -} - -export interface LensEmbeddableDeps { - attributeService: LensAttributeService; - data: DataPublicPluginStart; - documentToExpression: (doc: Document) => Promise<DocumentToExpressionReturnType>; - injectFilterReferences: FilterManager['inject']; - visualizationMap: VisualizationMap; - datasourceMap: DatasourceMap; - dataViews: DataViewsContract; - expressionRenderer: ReactExpressionRendererType; - timefilter: TimefilterContract; - basePath: IBasePath; - inspector: InspectorStart; - getTrigger?: UiActionsStart['getTrigger'] | undefined; - getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; - capabilities: { - canSaveVisualizations: boolean; - canOpenVisualizations: boolean; - canSaveDashboards: boolean; - navLinks: Capabilities['navLinks']; - discover: Capabilities['discover']; - }; - coreStart: CoreStart; - usageCollection?: UsageCollectionSetup; - spaces?: SpacesPluginStart; - uiSettings: IUiSettingsClient; -} - -export interface ViewUnderlyingDataArgs { - dataViewSpec: DataViewSpec; - timeRange: TimeRange; - filters: Filter[]; - query: Query | AggregateQuery | undefined; - columns: string[]; -} - -function VisualizationErrorPanel({ errors, canEdit }: { errors: UserMessage[]; canEdit: boolean }) { - const firstError = errors.at(0); - const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor); - return ( - <div className="lnsEmbeddedError"> - <EuiEmptyPrompt - iconType="warning" - iconColor="danger" - data-test-subj="embeddable-lens-failure" - body={ - <> - {firstError ? ( - <> - <p>{getLongMessage(firstError)}</p> - {errors.length > 1 && !canFixInLens ? ( - <p> - <FormattedMessage - id="xpack.lens.embeddable.moreErrors" - defaultMessage="Edit in Lens editor to see more errors" - /> - </p> - ) : null} - {canFixInLens ? ( - <p> - <FormattedMessage - id="xpack.lens.embeddable.fixErrors" - defaultMessage="Edit in Lens editor to fix the error" - /> - </p> - ) : null} - </> - ) : ( - <p> - <FormattedMessage - id="xpack.lens.embeddable.failure" - defaultMessage="Visualization couldn't be displayed" - /> - </p> - )} - </> - } - /> - </div> - ); -} - -const getExpressionFromDocument = async ( - document: Document, - documentToExpression: LensEmbeddableDeps['documentToExpression'] -) => { - const { ast, indexPatterns, indexPatternRefs, activeVisualizationState } = - await documentToExpression(document); - return { - ast: ast ? toExpression(ast) : null, - indexPatterns, - indexPatternRefs, - activeVisualizationState, - }; -}; - -function getViewUnderlyingDataArgs({ - activeDatasource, - activeDatasourceState, - activeVisualization, - activeVisualizationState, - activeData, - dataViews, - capabilities, - query, - filters, - timeRange, - esQueryConfig, - indexPatternsCache, -}: { - activeDatasource: Datasource; - activeDatasourceState: unknown; - activeVisualization: Visualization; - activeVisualizationState: unknown; - activeData: TableInspectorAdapter | undefined; - dataViews: DataViewBase[] | undefined; - capabilities: LensEmbeddableDeps['capabilities']; - query: ExecutionContextSearch['query']; - filters: Filter[]; - timeRange: TimeRange; - esQueryConfig: EsQueryConfig; - indexPatternsCache: IndexPatternMap; -}) { - const { error, meta } = getLayerMetaInfo( - activeDatasource, - activeDatasourceState, - activeVisualization, - activeVisualizationState, - activeData, - indexPatternsCache, - timeRange, - capabilities - ); - - if (error || !meta) { - return; - } - const luceneOrKuery: Query[] = []; - const aggregateQuery: AggregateQuery[] = []; - - if (Array.isArray(query)) { - query.forEach((q) => { - if (isOfQueryType(q)) { - luceneOrKuery.push(q); - } else { - aggregateQuery.push(q); - } - }); - } - - const { filters: newFilters, query: newQuery } = combineQueryAndFilters( - luceneOrKuery.length > 0 ? luceneOrKuery : (query as Query), - filters, - meta, - dataViews, - esQueryConfig - ); - - const dataViewSpec = indexPatternsCache[meta.id]!.spec; - - return { - dataViewSpec, - timeRange, - filters: newFilters, - query: aggregateQuery.length > 0 ? aggregateQuery[0] : newQuery, - columns: meta.columns, - }; -} - -const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) => { - const { euiTheme } = useEuiTheme(); - const xsFontSize = useEuiFontSize('xs').fontSize; - - if (!messages.length) { - return null; - } - - return ( - <MessageList - messages={messages} - customButtonStyles={css` - block-size: ${euiTheme.size.l}; - font-size: ${xsFontSize}; - padding: 0 ${euiTheme.size.xs}; - & > * { - gap: ${euiTheme.size.xs}; - } - `} - /> - ); -}; - -const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [ - 'visualization', - 'visualizationOnEmbeddable', -]; - -const MessagesBadge = ({ onMount }: { onMount: (el: HTMLDivElement) => void }) => ( - <div - css={css({ - position: 'absolute', - zIndex: 2, - left: 0, - bottom: 0, - })} - ref={(el) => { - if (el) { - onMount(el); - } - }} - /> -); - -export class Embeddable - extends AbstractEmbeddable<LensEmbeddableInput, LensEmbeddableOutput> - implements - ReferenceOrValueEmbeddable<LensByValueInput, LensByReferenceInput>, - SelfStyledEmbeddable, - FilterableEmbeddable -{ - type = DOC_TYPE; - - deferEmbeddableLoad = true; - - private expressionRenderer: ReactExpressionRendererType; - private savedVis: Document | undefined; - private expression: string | undefined | null; - private domNode: HTMLElement | Element | undefined; - private isInitialized = false; - private inputReloadSubscriptions: Subscription[]; - private isDestroyed?: boolean; - private lensInspector: LensInspector; - - private logError(type: 'runtime' | 'validation') { - trackUiCounterEvents( - type === 'runtime' ? 'embeddable_runtime_error' : 'embeddable_validation_error', - this.getExecutionContext() - ); - } - - private activeData?: TableInspectorAdapter; - - private internalDataViews: DataView[] = []; - - private viewUnderlyingDataArgs?: ViewUnderlyingDataArgs; - - private activeVisualizationState?: unknown; - - constructor( - private deps: LensEmbeddableDeps, - initialInput: LensEmbeddableInput, - parent?: IContainer - ) { - super( - initialInput, - { - editApp: 'lens', - }, - parent - ); - - this.lensInspector = getLensInspectorService(deps.inspector); - this.expressionRenderer = deps.expressionRenderer; - this.initializeSavedVis(initialInput) - .then(() => { - this.reload(); - }) - .catch((e) => this.onFatalError(e)); - - const input$ = this.getInput$(); - - this.inputReloadSubscriptions = []; - - // Lens embeddable does not re-render when embeddable input changes in - // general, to improve performance. This line makes sure the Lens embeddable - // re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes. - this.inputReloadSubscriptions.push( - input$ - .pipe( - map((input) => input.enhancements?.dynamicActions), - distinctUntilChanged((a, b) => fastIsEqual(a, b)), - skip(1) - ) - .subscribe((_input) => { - this.reload(); - }) - ); - - // Lens embeddable does not re-render when embeddable input changes in - // general, to improve performance. This line makes sure the Lens embeddable - // re-renders when dashboard view mode switches between "view/edit". This is - // needed to see the changes to ".dynamicActions" (e.g. drilldowns) when - // dashboard's mode is toggled. - this.inputReloadSubscriptions.push( - input$ - .pipe( - map((input) => input.viewMode), - distinctUntilChanged(), - skip(1) - ) - .subscribe((_input) => { - // only reload if drilldowns are set - if (this.getInput().enhancements?.dynamicActions) { - this.reload(); - } - }) - ); - - // Use a trigger to distinguish between observables in the subscription - const withTrigger = (trigger: 'attributesOrSavedObjectId' | 'searchContext') => - map((input: LensEmbeddableInput) => ({ trigger, input })); - - // Re-initialize the visualization if either the attributes or the saved object id changes - const attributesOrSavedObjectId$ = input$.pipe( - distinctUntilChanged((a, b) => - fastIsEqual( - [ - 'attributes' in a && a.attributes, - 'savedObjectId' in a && a.savedObjectId, - 'overrides' in a && a.overrides, - 'disableTriggers' in a && a.disableTriggers, - ], - [ - 'attributes' in b && b.attributes, - 'savedObjectId' in b && b.savedObjectId, - 'overrides' in b && b.overrides, - 'disableTriggers' in b && b.disableTriggers, - ] - ) - ), - skip(1), - withTrigger('attributesOrSavedObjectId') - ); - - // Update search context and reload on changes related to search - const searchContext$ = shouldFetch$<LensEmbeddableInput>(input$, () => this.getInput()).pipe( - withTrigger('searchContext') - ); - - // Merge and debounce the observables to avoid multiple reloads - this.inputReloadSubscriptions.push( - merge(searchContext$, attributesOrSavedObjectId$) - .pipe( - debounceTime(0), - switchMap(async ({ trigger, input }) => { - if (trigger === 'attributesOrSavedObjectId') { - await this.initializeSavedVis(input); - } - - // reset removable messages - // Dashboard search/context changes are detected here - this.additionalUserMessages = {}; - - this.reload(); - }) - ) - .subscribe() - ); - } - - private get activeDatasourceId() { - return getActiveDatasourceIdFromDoc(this.savedVis); - } - - private get activeDatasource() { - if (!this.activeDatasourceId) return; - return this.deps.datasourceMap[this.activeDatasourceId]; - } - - private get activeVisualizationId() { - return getActiveVisualizationIdFromDoc(this.savedVis); - } - - private get activeVisualization() { - if (!this.activeVisualizationId) return; - return this.deps.visualizationMap[this.activeVisualizationId]; - } - - private indexPatterns: IndexPatternMap = {}; - - private indexPatternRefs: IndexPatternRef[] = []; - - // TODO - consider getting this from the persistedStateToExpression function - // where it is already computed - private get activeDatasourceState(): undefined | unknown { - if (!this.activeDatasourceId || !this.activeDatasource) return; - - const docDatasourceState = this.savedVis?.state.datasourceStates[this.activeDatasourceId]; - - return this.activeDatasource.initialize( - docDatasourceState, - [...(this.savedVis?.references || []), ...(this.savedVis?.state.internalReferences || [])], - undefined, - undefined, - this.indexPatterns - ); - } - - private fullAttributes: LensSavedObjectAttributes | undefined; - - private handleExternalUserMessage = (messages: UserMessage[]) => { - if (this.input.onBeforeBadgesRender) { - // we need something else to better identify those errors - const [messagesToHandle, originalMessages] = partition(messages, (message) => - message.displayLocations.some((location) => location.id === 'embeddableBadge') - ); - - if (messagesToHandle.length > 0) { - const customBadgeMessages = this.input.onBeforeBadgesRender(messagesToHandle); - return [...originalMessages, ...customBadgeMessages]; - } - } - - return messages; - }; - - public getUserMessages: UserMessagesGetter = (locationId, filters) => { - const userMessages: UserMessage[] = []; - userMessages.push( - ...getApplicationUserMessages({ - visualizationType: this.savedVis?.visualizationType, - visualizationState: { - state: this.activeVisualizationState, - activeId: this.activeVisualizationId, - }, - visualization: - this.activeVisualizationId && this.deps.visualizationMap[this.activeVisualizationId] - ? this.deps.visualizationMap[this.activeVisualizationId] - : undefined, - activeDatasource: this.activeDatasource, - activeDatasourceState: { - isLoading: !this.activeDatasourceState, - state: this.activeDatasourceState, - }, - dataViews: { - indexPatterns: this.indexPatterns, - indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used? - }, - core: this.deps.coreStart, - }) - ); - - if (!this.savedVis) { - return this.handleExternalUserMessage(userMessages); - } - - const mergedSearchContext = this.getMergedSearchContext(); - - const framePublicAPI: FramePublicAPI = { - dataViews: { - indexPatterns: this.indexPatterns, - indexPatternRefs: this.indexPatternRefs, - }, - datasourceLayers: getDatasourceLayers( - { - [this.activeDatasourceId!]: { - isLoading: !this.activeDatasourceState, - state: this.activeDatasourceState, - }, - }, - this.deps.datasourceMap, - this.indexPatterns - ), - query: this.savedVis.state.query, - filters: mergedSearchContext.filters ?? [], - dateRange: { - fromDate: mergedSearchContext.timeRange?.from ?? '', - toDate: mergedSearchContext.timeRange?.to ?? '', - }, - absDateRange: { - fromDate: mergedSearchContext.timeRange?.from ?? '', - toDate: mergedSearchContext.timeRange?.to ?? '', - }, - activeData: this.activeData, - }; - - userMessages.push( - ...(this.activeDatasource?.getUserMessages(this.activeDatasourceState, { - setState: () => {}, - frame: framePublicAPI, - visualizationInfo: this.activeVisualization?.getVisualizationInfo?.( - this.activeVisualizationState, - framePublicAPI - ), - }) ?? []), - ...(this.activeVisualization?.getUserMessages?.(this.activeVisualizationState, { - frame: framePublicAPI, - }) ?? []) - ); - - return this.handleExternalUserMessage( - filterAndSortUserMessages( - [...userMessages, ...Object.values(this.additionalUserMessages)], - locationId, - filters ?? {} - ) - ); - }; - - private additionalUserMessages: Record<string, UserMessage> = {}; - - // used to add warnings and errors from elsewhere in the embeddable - private addUserMessages: AddUserMessages = (messages) => { - const newMessageMap = { - ...this.additionalUserMessages, - }; - - const addedMessageIds: string[] = []; - messages.forEach((message) => { - if (!newMessageMap[message.uniqueId]) { - addedMessageIds.push(message.uniqueId); - newMessageMap[message.uniqueId] = message; - } - }); - - if (addedMessageIds.length) { - this.additionalUserMessages = newMessageMap; - this.renderUserMessages(); - } - - return () => { - messages.forEach(({ uniqueId }) => { - delete this.additionalUserMessages[uniqueId]; - }); - }; - }; - - public reportsEmbeddableLoad() { - return true; - } - - public supportedTriggers() { - if (!this.savedVis || !this.savedVis.visualizationType) { - return []; - } - - return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || []; - } - - public getInspectorAdapters() { - return this.lensInspector.adapters; - } - - public getFullAttributes() { - return this.fullAttributes; - } - - public isTextBasedLanguage() { - if (!this.savedVis) { - return; - } - const query = this.savedVis.state.query; - return !isOfQueryType(query); - } - - public getTextBasedLanguage(): string | undefined { - if (!this.isTextBasedLanguage() || !this.savedVis?.state.query) { - return; - } - const query = this.savedVis?.state.query as unknown as AggregateQuery; - const language = getAggregateQueryMode(query); - return getLanguageDisplayName(language).toUpperCase(); - } - - /** - * Gets the Lens embeddable's datasource and visualization states - * updates the embeddable input - */ - async updateVisualization( - datasourceState: unknown, - visualizationState: unknown, - visualizationType?: string - ) { - const viz = this.savedVis; - const activeDatasourceId = (this.activeDatasourceId ?? - 'formBased') as EditLensConfigurationProps['datasourceId']; - if (viz?.state) { - const datasourceStates = { - ...viz.state.datasourceStates, - [activeDatasourceId]: datasourceState, - }; - const references = - activeDatasourceId === 'formBased' - ? extractReferencesFromState({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: this.deps.datasourceMap[datasourceId], - }), - {} - ), - datasourceStates: Object.fromEntries( - Object.entries(datasourceStates).map(([id, state]) => [ - id, - { isLoading: false, state }, - ]) - ), - visualizationState, - activeVisualization: this.activeVisualizationId - ? this.deps.visualizationMap[visualizationType ?? this.activeVisualizationId] - : undefined, - }) - : []; - const attrs = { - ...viz, - state: { - ...viz.state, - visualization: visualizationState, - datasourceStates, - }, - references, - visualizationType: visualizationType ?? viz.visualizationType, - }; - - /** - * SavedObjectId is undefined for by value panels and defined for the by reference ones. - * Here we are converting the by reference panels to by value when user is inline editing - */ - this.updateInput({ attributes: attrs, savedObjectId: undefined }); - /** - * Should load again the user messages, - * otherwise the embeddable state is stuck in an error state - */ - this.renderUserMessages(); - } - } - - async updateSuggestion(attrs: LensSavedObjectAttributes) { - const viz = this.savedVis; - const newViz = { - ...viz, - ...attrs, - }; - this.updateInput({ attributes: newViz }); - } - - /** - * Callback which allows the navigation to the editor. - * Used for the Edit in Lens link inside the inline editing flyout. - */ - private async navigateToLensEditor() { - const appContext = this.getAppContext(); - /** - * The origininating app variable is very important for the Save and Return button - * of the editor to work properly. - */ - const transferState = { - originatingApp: appContext?.currentAppId ?? 'dashboards', - originatingPath: appContext?.getCurrentPath?.(), - valueInput: this.getExplicitInput(), - embeddableId: this.id, - searchSessionId: this.getInput().searchSessionId, - }; - const transfer = new EmbeddableStateTransfer( - this.deps.coreStart.application.navigateToApp, - this.deps.coreStart.application.currentAppId$ - ); - if (transfer) { - await transfer.navigateToEditor(APP_ID, { - path: this.output.editPath, - state: transferState, - skipAppLeave: true, - }); - } - } - - public updateByRefInput(savedObjectId: string) { - const attrs = this.savedVis; - this.updateInput({ attributes: attrs, savedObjectId }); - } - - async openConfigPanel( - startDependencies: LensPluginStartDependencies, - isNewPanel?: boolean, - deletePanel?: () => void - ) { - const { getEditLensConfiguration } = await import('../async_services'); - const Component = await getEditLensConfiguration( - this.deps.coreStart, - startDependencies, - this.deps.visualizationMap, - this.deps.datasourceMap - ); - - const datasourceId = (this.activeDatasourceId ?? - 'formBased') as EditLensConfigurationProps['datasourceId']; - - const attributes = this.savedVis as TypedLensByValueInput['attributes']; - if (attributes) { - return ( - <Component - attributes={attributes} - updatePanelState={this.updateVisualization.bind(this)} - updateSuggestion={this.updateSuggestion.bind(this)} - datasourceId={datasourceId} - lensAdapters={this.lensInspector.adapters} - output$={this.getOutput$()} - panelId={this.id} - savedObjectId={this.savedVis?.savedObjectId} - updateByRefInput={this.updateByRefInput.bind(this)} - navigateToLensEditor={ - !this.isTextBasedLanguage() ? this.navigateToLensEditor.bind(this) : undefined - } - displayFlyoutHeader - canEditTextBasedQuery={this.isTextBasedLanguage()} - isNewPanel={isNewPanel} - deletePanel={deletePanel} - /> - ); - } - return null; - } - - async initializeSavedVis(input: LensEmbeddableInput) { - const unwrapResult: LensUnwrapResult | false = await this.deps.attributeService - .unwrapAttributes(input) - .catch((e: Error) => { - this.onFatalError(e); - return false; - }); - if (!unwrapResult || this.isDestroyed) { - return; - } - - const { metaInfo, attributes } = unwrapResult; - this.fullAttributes = attributes; - this.savedVis = { - ...attributes, - type: this.type, - savedObjectId: (input as LensByReferenceInput)?.savedObjectId, - }; - - if (this.isTextBasedLanguage()) { - this.updateInput({ - disabledActions: ['OPEN_FLYOUT_ADD_DRILLDOWN'], - }); - } - - try { - const { ast, indexPatterns, indexPatternRefs, activeVisualizationState } = - await getExpressionFromDocument(this.savedVis, this.deps.documentToExpression); - - this.expression = ast; - this.indexPatterns = indexPatterns; - this.indexPatternRefs = indexPatternRefs; - this.activeVisualizationState = activeVisualizationState; - } catch { - // nothing, errors should be reported via getUserMessages - } - - if (metaInfo?.sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) { - this.addUserMessages([ - { - uniqueId: 'url-conflict', - severity: 'error', - displayLocations: [{ id: 'visualization' }], - shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { - defaultMessage: `You've encountered a URL conflict`, - }), - longMessage: ( - <this.deps.spaces.ui.components.getEmbeddableLegacyUrlConflict - targetType={DOC_TYPE} - sourceId={metaInfo?.sharingSavedObjectProps?.sourceId!} - /> - ), - fixableInEditor: false, - }, - ]); - } - - await this.initializeOutput(); - - // deferred loading of this embeddable is complete - this.setInitializationFinished(); - - this.isInitialized = true; - } - - private getSearchWarningMessages(adapters?: Partial<DefaultInspectorAdapters>): UserMessage[] { - if (!this.activeDatasource || !this.activeDatasourceId || !adapters?.requests) { - return []; - } - - const docDatasourceState = this.savedVis?.state.datasourceStates[this.activeDatasourceId]; - - const requestWarnings = getSearchWarningMessages( - adapters.requests, - this.activeDatasource, - docDatasourceState, - { - searchService: this.deps.data.search, - } - ); - - return requestWarnings; - } - - private removeActiveDataWarningMessages: () => void = () => {}; - private updateActiveData: ExpressionWrapperProps['onData$'] = (data, adapters) => { - if (this.input.onLoad) { - // once onData$ is get's called from expression renderer, loading becomes false - this.input.onLoad(false, adapters, this.getOutput$()); - } - - const { type, error } = data as { type: string; error: ErrorLike }; - this.updateOutput({ - loading: false, - error: type === 'error' ? error : undefined, - }); - - const newActiveData = adapters?.tables?.tables; - - this.removeActiveDataWarningMessages(); - const searchWarningMessages = this.getSearchWarningMessages(adapters); - this.removeActiveDataWarningMessages = this.addUserMessages(searchWarningMessages); - - this.activeData = newActiveData; - - this.renderUserMessages(); - - this.loadViewUnderlyingDataArgs(); - }; - - private onRender: ExpressionWrapperProps['onRender$'] = () => { - let datasourceEvents: string[] = []; - let visualizationEvents: string[] = []; - - if (this.savedVis) { - datasourceEvents = Object.values(this.deps.datasourceMap).reduce<string[]>( - (acc, datasource) => [ - ...acc, - ...(datasource.getRenderEventCounters?.( - this.savedVis!.state.datasourceStates[datasource.id] - ) ?? []), - ], - [] - ); - - if (this.savedVis.visualizationType) { - visualizationEvents = - this.deps.visualizationMap[this.savedVis.visualizationType].getRenderEventCounters?.( - this.savedVis!.state.visualization - ) ?? []; - } - } - - const executionContext = this.getExecutionContext(); - - const events = [ - ...datasourceEvents, - ...visualizationEvents, - ...getExecutionContextEvents(executionContext), - ]; - - const adHocDataViews = Object.values(this.savedVis?.state.adHocDataViews || {}); - adHocDataViews.forEach(() => { - events.push('ad_hoc_data_view'); - }); - - trackUiCounterEvents(events, executionContext); - this.trackContentfulRender(); - - this.renderComplete.dispatchComplete(); - this.updateOutput({ - ...this.getOutput(), - rendered: true, - }); - - const inspectorAdapters = this.getInspectorAdapters(); - const timings = getSuccessfulRequestTimings(inspectorAdapters); - if (timings) { - const esRequestMetrics = { - eventName: 'lens_chart_es_request_totals', - duration: timings.requestTimeTotal, - key1: 'es_took_total', - value1: timings.esTookTotal, - }; - reportPerformanceMetricEvent(this.deps.coreStart.analytics, esRequestMetrics); - } - }; - - getExecutionContext() { - if (this.savedVis) { - const parentContext = this.parent?.getInput().executionContext || this.input.executionContext; - const child: KibanaExecutionContext = { - type: 'lens', - name: this.savedVis.visualizationType ?? '', - id: this.id, - description: this.savedVis.title || this.input.title || '', - url: this.output.editUrl, - }; - - return parentContext - ? { - ...parentContext, - child, - } - : child; - } - } - - /** - * - * @param {HTMLElement} domNode - * @param {ContainerState} containerState - */ - render(domNode: HTMLElement | Element) { - this.domNode = domNode; - if (!this.savedVis || !this.isInitialized || this.isDestroyed) { - return; - } - super.render(domNode as HTMLElement); - - if (this.input.onLoad) { - this.input.onLoad(true); - } - - this.domNode.setAttribute('data-shared-item', ''); - - const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, { - severity: 'error', - }); - - this.updateOutput({ - loading: true, - error: blockingErrors.length - ? new Error( - typeof blockingErrors[0].longMessage === 'string' - ? blockingErrors[0].longMessage - : blockingErrors[0].shortMessage - ) - : undefined, - }); - - if (blockingErrors.length) { - this.renderComplete.dispatchError(); - } else { - this.renderComplete.dispatchInProgress(); - } - - const input = this.getInput(); - - const getInternalTables = (states: Record<string, unknown>) => { - const result: Record<string, Datatable> = {}; - if ('textBased' in states) { - const layers = (states.textBased as TextBasedPersistedState).layers; - for (const layer in layers) { - if (layers[layer] && layers[layer].table) { - result[layer] = layers[layer].table!; - } - } - } - return result; - }; - - if (this.expression && !blockingErrors.length) { - render( - <> - <KibanaRenderContextProvider {...this.deps.coreStart}> - <ExpressionWrapper - ExpressionRenderer={this.expressionRenderer} - expression={this.expression || null} - lensInspector={this.lensInspector} - searchContext={this.getMergedSearchContext()} - variables={{ - embeddableTitle: this.getTitle(), - ...(input.palette ? { theme: { palette: input.palette } } : {}), - ...('overrides' in input ? { overrides: input.overrides } : {}), - ...getInternalTables(this.savedVis.state.datasourceStates), - }} - searchSessionId={this.getInput().searchSessionId} - handleEvent={this.handleEvent} - onData$={this.updateActiveData} - onRender$={this.onRender} - interactive={!input.disableTriggers} - renderMode={input.renderMode} - syncColors={input.syncColors} - syncTooltips={input.syncTooltips} - syncCursor={input.syncCursor} - hasCompatibleActions={this.hasCompatibleActions} - getCompatibleCellValueActions={this.getCompatibleCellValueActions} - className={input.className} - style={input.style} - executionContext={this.getExecutionContext()} - abortController={this.input.abortController} - addUserMessages={(messages) => this.addUserMessages(messages)} - onRuntimeError={(error) => { - this.updateOutput({ error }); - this.logError('runtime'); - }} - noPadding={this.visDisplayOptions.noPadding} - /> - </KibanaRenderContextProvider> - <MessagesBadge - onMount={(el) => { - this.badgeDomNode = el; - this.renderBadgeMessages(); - }} - /> - </>, - domNode - ); - } - - this.renderUserMessages(); - } - - private trackContentfulRender() { - if (!this.activeData || !canTrackContentfulRender(this.parent)) { - return; - } - - const hasData = Object.values(this.activeData).some((table) => { - if (table.meta?.statistics?.totalCount != null) { - // if totalCount is set, refer to total count - return table.meta.statistics.totalCount > 0; - } - // if not, fall back to check the rows of the table - return table.rows.length > 0; - }); - - if (hasData) { - this.parent.trackContentfulRender(); - } - } - - private renderUserMessages() { - const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], { - severity: 'error', - }); - - if (errors.length && this.domNode) { - render( - <> - <KibanaRenderContextProvider {...this.deps.coreStart}> - <VisualizationErrorPanel - errors={errors} - canEdit={this.getIsEditable() && this.input.viewMode === 'edit'} - /> - </KibanaRenderContextProvider> - <MessagesBadge - onMount={(el) => { - this.badgeDomNode = el; - this.renderBadgeMessages(); - }} - /> - </>, - this.domNode - ); - } - - this.renderBadgeMessages(); - } - - badgeDomNode?: HTMLDivElement; - - /** - * This method is called on every render, and also whenever the badges dom node is created - * That happens after either the expression renderer or the visualization error panel is rendered. - * - * You should not call this method on its own. Use renderUserMessages instead. - */ - private renderBadgeMessages = () => { - const messages = this.getUserMessages('embeddableBadge'); - const [warningOrErrorMessages, infoMessages] = partition( - messages, - ({ severity }) => severity !== 'info' - ); - - if (this.badgeDomNode) { - render( - <KibanaRenderContextProvider {...this.deps.coreStart}> - <EmbeddableMessagesPopover messages={warningOrErrorMessages} /> - <EmbeddableFeatureBadge messages={infoMessages} /> - </KibanaRenderContextProvider>, - this.badgeDomNode - ); - } - }; - - private readonly hasCompatibleActions = async ( - event: ExpressionRendererEvent - ): Promise<boolean> => { - if ( - isLensTableRowContextMenuClickEvent(event) || - isLensMultiFilterEvent(event) || - isLensFilterEvent(event) - ) { - const { getTriggerCompatibleActions } = this.deps; - if (!getTriggerCompatibleActions) { - return false; - } - const actions = await getTriggerCompatibleActions(VIS_EVENT_TO_TRIGGER[event.name], { - data: event.data, - embeddable: this, - }); - - return actions.length > 0; - } - - return false; - }; - - private readonly getCompatibleCellValueActions: GetCompatibleCellValueActions = async (data) => { - const { getTriggerCompatibleActions } = this.deps; - if (getTriggerCompatibleActions) { - const embeddable = this; - const actions: Array<Action<CellValueContext>> = (await getTriggerCompatibleActions( - CELL_VALUE_TRIGGER, - { data, embeddable } - )) as Array<Action<CellValueContext>>; - return actions - .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) - .map((action) => ({ - id: action.id, - type: action.type, - iconType: action.getIconType({ embeddable, data, trigger: cellValueTrigger })!, - displayName: action.getDisplayName({ embeddable, data, trigger: cellValueTrigger }), - execute: (cellData) => - action.execute({ embeddable, data: cellData, trigger: cellValueTrigger }), - })); - } - return []; - }; - - /** - * Combines the embeddable context with the saved object context, and replaces - * any references to index patterns - */ - private getMergedSearchContext(): ExecutionContextSearch { - if (!this.savedVis) { - throw new Error('savedVis is required for getMergedSearchContext'); - } - - const input = this.getInput(); - const context: ExecutionContextSearch = { - now: this.deps.data.nowProvider.get().getTime(), - timeRange: - input.timeslice !== undefined - ? { - from: new Date(input.timeslice[0]).toISOString(), - to: new Date(input.timeslice[1]).toISOString(), - mode: 'absolute' as 'absolute', - } - : input.timeRange, - query: [this.savedVis.state.query], - filters: this.deps.injectFilterReferences( - this.savedVis.state.filters, - this.savedVis.references - ), - disableWarningToasts: true, - }; - - if (input.query) { - context.query = [input.query, ...(context.query as Query[])]; - } - - if (input.filters?.length) { - context.filters = [ - ...input.filters.filter((filter) => !filter.meta.disabled), - ...(context.filters as Filter[]), - ]; - } - - return context; - } - - private get onEditAction(): Visualization['onEditAction'] { - const visType = this.savedVis?.visualizationType; - - if (!visType) { - return; - } - - return this.deps.visualizationMap[visType].onEditAction; - } - - handleEvent = async (event: ExpressionRendererEvent) => { - if (!this.deps.getTrigger || this.input.disableTriggers) { - return; - } - - let eventHandler: - | LensBaseEmbeddableInput['onBrushEnd'] - | LensBaseEmbeddableInput['onFilter'] - | LensBaseEmbeddableInput['onTableRowClick']; - let shouldExecuteDefaultTriggers = true; - - if (isLensBrushEvent(event)) { - eventHandler = this.input.onBrushEnd; - } else if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) { - eventHandler = this.input.onFilter; - } else if (isLensTableRowContextMenuClickEvent(event)) { - eventHandler = this.input.onTableRowClick; - } - // if the embeddable is located in an app where there is the Unified search bar with the ES|QL editor, then use this query - // otherwise use the query from the saved object - let esqlQuery: AggregateQuery | Query | undefined; - if (this.isTextBasedLanguage()) { - const query = this.deps.data.query.queryString.getQuery(); - esqlQuery = isOfAggregateQueryType(query) ? query : this.savedVis?.state.query; - } - - eventHandler?.({ - ...event.data, - preventDefault: () => { - shouldExecuteDefaultTriggers = false; - }, - }); - - if (isLensFilterEvent(event) || isLensMultiFilterEvent(event) || isLensBrushEvent(event)) { - if (shouldExecuteDefaultTriggers) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: { - ...event.data, - timeFieldName: - event.data.timeFieldName || inferTimeField(this.deps.data.datatableUtilities, event), - query: esqlQuery, - }, - embeddable: this, - }); - } - } - - if (isLensTableRowContextMenuClickEvent(event)) { - if (shouldExecuteDefaultTriggers) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( - { - data: event.data, - embeddable: this, - }, - true - ); - } - } - - // We allow for edit actions in the Embeddable for display purposes only (e.g. changing the datatable sort order). - // No state changes made here with an edit action are persisted. - if (isLensEditEvent(event) && this.onEditAction) { - if (!this.savedVis) return; - - // have to dance since this.savedVis.state is readonly - const newVis = JSON.parse(JSON.stringify(this.savedVis)) as Document; - newVis.state.visualization = this.onEditAction(newVis.state.visualization, event); - this.savedVis = newVis; - - const { ast } = await getExpressionFromDocument( - this.savedVis, - this.deps.documentToExpression - ); - - this.expression = ast; - - this.reload(); - } - }; - - reload() { - if (!this.savedVis || !this.isInitialized || this.isDestroyed) { - return; - } - - if (this.domNode) { - this.render(this.domNode); - } - } - - private async loadViewUnderlyingDataArgs(): Promise<void> { - if ( - !this.savedVis || - !this.activeData || - !this.activeDatasource || - !this.activeDatasourceState || - !this.activeVisualization || - !this.activeVisualizationState - ) { - this.canViewUnderlyingData$.next(false); - return; - } - - const mergedSearchContext = this.getMergedSearchContext(); - - if (!mergedSearchContext.timeRange) { - this.canViewUnderlyingData$.next(false); - return; - } - - const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({ - activeDatasource: this.activeDatasource, - activeDatasourceState: this.activeDatasourceState, - activeVisualization: this.activeVisualization, - activeVisualizationState: this.activeVisualizationState, - activeData: this.activeData, - dataViews: this.internalDataViews, - capabilities: this.deps.capabilities, - query: mergedSearchContext.query, - filters: mergedSearchContext.filters || [], - timeRange: mergedSearchContext.timeRange, - esQueryConfig: getEsQueryConfig(this.deps.uiSettings), - indexPatternsCache: this.indexPatterns, - }); - - const loaded = typeof viewUnderlyingDataArgs !== 'undefined'; - if (loaded) { - this.viewUnderlyingDataArgs = viewUnderlyingDataArgs; - } - - this.canViewUnderlyingData$.next(loaded); - } - - /** - * Returns the necessary arguments to view the underlying data in discover. - * - * Only makes sense to call this after canViewUnderlyingData has been checked - */ - public getViewUnderlyingDataArgs() { - return this.viewUnderlyingDataArgs; - } - - public canViewUnderlyingData$ = new BehaviorSubject<boolean>(false); - - async initializeOutput() { - if (!this.savedVis) { - return; - } - - const { indexPatterns } = await getIndexPatternsObjects( - this.savedVis?.references.map(({ id }) => id) || [], - this.deps.dataViews - ); - ( - await Promise.all( - Object.values(this.savedVis?.state.adHocDataViews || {}).map((spec) => - this.deps.dataViews.create(spec) - ) - ) - ).forEach((dataView) => indexPatterns.push(dataView)); - - this.internalDataViews = uniqBy(indexPatterns, 'id'); - - // passing edit url and index patterns to the output of this embeddable for - // the container to pick them up and use them to configure filter bar and - // config dropdown correctly. - const input = this.getInput(); - - // if at least one indexPattern is time based, then the Lens embeddable requires the timeRange prop - // this is necessary for the dataview embeddable but not the ES|QL one - if ( - !Boolean(this.isTextBasedLanguage()) && - input.timeRange == null && - indexPatterns.some((indexPattern) => indexPattern.isTimeBased()) - ) { - this.addUserMessages([ - { - uniqueId: 'missing-time-range-on-embeddable', - severity: 'error', - fixableInEditor: false, - displayLocations: [{ id: 'visualization' }], - shortMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.shortMessage', { - defaultMessage: `Missing timeRange property`, - }), - longMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.longMessage', { - defaultMessage: `The timeRange property is required for the given configuration`, - }), - }, - ]); - } - - const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, { - severity: 'error', - }); - if (blockingErrors.length) { - this.logError('validation'); - } - - const title = input.hidePanelTitles ? '' : input.title ?? this.savedVis.title; - const description = input.hidePanelTitles ? '' : input.description ?? this.savedVis.description; - const savedObjectId = (input as LensByReferenceInput).savedObjectId; - this.updateOutput({ - defaultTitle: this.savedVis.title, - defaultDescription: this.savedVis.description, - /** lens visualizations allow inline editing action - * navigation to the editor is allowed through the flyout - */ - editable: this.getIsEditable(), - inlineEditable: true, - title, - description, - editPath: getEditPath(savedObjectId), - editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), - indexPatterns: this.internalDataViews, - }); - } - - public getIsEditable() { - // for ES|QL, editing is allowed only if the advanced setting is on - if (Boolean(this.isTextBasedLanguage()) && !this.deps.uiSettings.get(ENABLE_ESQL)) { - return false; - } - return ( - this.deps.capabilities.canSaveVisualizations || - (!this.inputIsRefType(this.getInput()) && - this.deps.capabilities.canSaveDashboards && - this.deps.capabilities.canOpenVisualizations) - ); - } - - public inputIsRefType = ( - input: LensByValueInput | LensByReferenceInput - ): input is LensByReferenceInput => { - return this.deps.attributeService.inputIsRefType(input); - }; - - public getInputAsRefType = async (): Promise<LensByReferenceInput> => { - return this.deps.attributeService.getInputAsRefType(this.getExplicitInput(), { - showSaveModal: true, - saveModalTitle: this.getTitle(), - }); - }; - - public getInputAsValueType = async (): Promise<LensByValueInput> => { - return this.deps.attributeService.getInputAsValueType(this.getExplicitInput()); - }; - - /** - * Gets the Lens embeddable's local filters - * @returns Local/panel-level array of filters for Lens embeddable - */ - public getFilters() { - try { - return mapAndFlattenFilters( - this.deps.injectFilterReferences( - this.savedVis?.state.filters ?? [], - this.savedVis?.references ?? [] - ) - ); - } catch (e) { - // if we can't parse the filters, we publish an empty array. - return []; - } - } - - /** - * Gets the Lens embeddable's local query - * @returns Local/panel-level query for Lens embeddable - */ - public getQuery() { - return this.savedVis?.state.query; - } - - public getSavedVis(): Readonly<LensSavedObjectAttributes | undefined> { - if (!this.savedVis) { - return; - } - - // Why are 'type' and 'savedObjectId' keys being removed? - // Prior to removing them, - // this method returned 'Readonly<Document | undefined>' while consumers typed the results as 'LensSavedObjectAttributes'. - // Removing 'type' and 'savedObjectId' keys to align method results with consumer typing. - const savedVis = { ...this.savedVis }; - delete savedVis.type; - delete savedVis.savedObjectId; - return savedVis; - } - - destroy() { - this.isDestroyed = true; - super.destroy(); - if (this.inputReloadSubscriptions.length > 0) { - this.inputReloadSubscriptions.forEach((reloadSub) => { - reloadSub.unsubscribe(); - }); - } - if (this.domNode) { - unmountComponentAtNode(this.domNode); - } - } - - public getSelfStyledOptions() { - return { - hideTitle: this.visDisplayOptions.noPanelTitle, - }; - } - - private get visDisplayOptions(): VisualizationDisplayOptions { - if (!this.savedVis?.visualizationType) { - return {}; - } - - let displayOptions = - this.deps.visualizationMap[this.savedVis.visualizationType]?.getDisplayOptions?.() ?? {}; - - if (this.input.noPadding !== undefined) { - displayOptions = { - ...displayOptions, - noPadding: this.input.noPadding, - }; - } - - return displayOptions; - } -} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx deleted file mode 100644 index f433f71d453b8..0000000000000 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ /dev/null @@ -1,188 +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, useEffect } from 'react'; -import type { CoreStart } from '@kbn/core/public'; -import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; -import { PanelLoader } from '@kbn/panel-loader'; -import { EuiLoadingChart } from '@elastic/eui'; -import { - EmbeddableFactory, - EmbeddableInput, - EmbeddableOutput, - EmbeddablePanel, - EmbeddableRoot, - EmbeddableStart, - IEmbeddable, - useEmbeddableFactory, -} from '@kbn/embeddable-plugin/public'; -import type { LensByReferenceInput, LensByValueInput } from './embeddable'; -import type { Document } from '../persistence'; -import type { FormBasedPersistedState } from '../datasources/form_based/types'; -import type { TextBasedPersistedState } from '../datasources/text_based/types'; -import type { XYState } from '../visualizations/xy/types'; -import type { - PieVisualizationState, - LegacyMetricState, - AllowedGaugeOverrides, - AllowedPartitionOverrides, - AllowedSettingsOverrides, - AllowedXYOverrides, -} from '../../common/types'; -import type { DatatableVisualizationState } from '../visualizations/datatable/visualization'; -import type { MetricVisualizationState } from '../visualizations/metric/types'; -import type { HeatmapVisualizationState } from '../visualizations/heatmap/types'; -import type { GaugeVisualizationState } from '../visualizations/gauge/constants'; - -type LensAttributes<TVisType, TVisState> = Omit< - Document, - 'savedObjectId' | 'type' | 'state' | 'visualizationType' -> & { - visualizationType: TVisType; - state: Omit<Document['state'], 'datasourceStates' | 'visualization'> & { - datasourceStates: { - formBased?: FormBasedPersistedState; - textBased?: TextBasedPersistedState; - }; - visualization: TVisState; - }; -}; - -/** - * Type-safe variant of by value embeddable input for Lens. - * This can be used to hardcode certain Lens chart configurations within another app. - */ -export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes' | 'overrides'> & { - attributes: - | LensAttributes<'lnsXY', XYState> - | LensAttributes<'lnsPie', PieVisualizationState> - | LensAttributes<'lnsHeatmap', HeatmapVisualizationState> - | LensAttributes<'lnsGauge', GaugeVisualizationState> - | LensAttributes<'lnsDatatable', DatatableVisualizationState> - | LensAttributes<'lnsLegacyMetric', LegacyMetricState> - | LensAttributes<'lnsMetric', MetricVisualizationState> - | LensAttributes<string, unknown>; - - /** - * Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline. - * XY charts offer an override of the Settings ('settings') and Axis ('axisX', 'axisLeft', 'axisRight') components. - * While it is not possible to pass function/callback/handlers to the renderer, it is possible to stop them by passing the - * "ignore" string as override value (i.e. onBrushEnd: "ignore") - */ - overrides?: - | AllowedSettingsOverrides - | AllowedXYOverrides - | AllowedPartitionOverrides - | AllowedGaugeOverrides; -}; - -export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & { - withDefaultActions?: boolean; - extraActions?: Action[]; - showInspector?: boolean; - abortController?: AbortController; -}; - -export type EmbeddableComponent = React.ComponentType<EmbeddableComponentProps>; - -interface PluginsStartDependencies { - uiActions: UiActionsStart; - embeddable: EmbeddableStart; - inspector: InspectorStartContract; -} - -export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) { - const { embeddable: embeddableStart, uiActions } = plugins; - const factory = embeddableStart.getEmbeddableFactory('lens')!; - return (props: EmbeddableComponentProps) => { - const input = { ...props }; - const hasActions = - Boolean(input.withDefaultActions) || (input.extraActions && input.extraActions?.length > 0); - - if (hasActions) { - return ( - <EmbeddablePanelWrapper - factory={factory} - uiActions={uiActions} - actionPredicate={() => hasActions} - input={input} - extraActions={input.extraActions} - showInspector={input.showInspector} - withDefaultActions={input.withDefaultActions} - /> - ); - } - return <EmbeddableRootWrapper factory={factory} input={input} />; - }; -} - -function EmbeddableRootWrapper({ - factory, - input, -}: { - factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>; - input: EmbeddableComponentProps; -}) { - const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); - if (loading) { - return <EuiLoadingChart />; - } - return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />; -} - -interface EmbeddablePanelWrapperProps { - factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>; - uiActions: PluginsStartDependencies['uiActions']; - actionPredicate: (id: string) => boolean; - input: EmbeddableComponentProps; - extraActions?: Action[]; - showInspector?: boolean; - withDefaultActions?: boolean; - abortController?: AbortController; -} - -const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({ - factory, - uiActions, - actionPredicate, - input, - extraActions, - showInspector = true, - withDefaultActions, - abortController, -}) => { - const [embeddable, loading] = useEmbeddableFactory({ factory, input }); - useEffect(() => { - if (embeddable) { - embeddable.updateInput(input); - } - }, [embeddable, input]); - - if (loading || !embeddable) { - return <PanelLoader />; - } - - return ( - <EmbeddablePanel - hideHeader={false} - embeddable={embeddable as IEmbeddable<EmbeddableInput, EmbeddableOutput>} - getActions={async (triggerId, context) => { - const actions = withDefaultActions - ? await uiActions.getTriggerCompatibleActions(triggerId, context) - : []; - - return [...(extraActions ?? []), ...actions]; - }} - hideInspector={!showInspector} - actionPredicate={actionPredicate} - showNotifications={false} - showShadow={false} - showBadges={false} - /> - ); -}; diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts deleted file mode 100644 index d84aca319a42b..0000000000000 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - Capabilities, - CoreStart, - HttpSetup, - IUiSettingsClient, - ThemeServiceStart, -} from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { RecursiveReadonly } from '@kbn/utility-types'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { DataPublicPluginStart, FilterManager, TimefilterContract } from '@kbn/data-plugin/public'; -import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public'; -import { - EmbeddableFactoryDefinition, - IContainer, - ErrorEmbeddable, -} from '@kbn/embeddable-plugin/public'; -import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import type { Start as InspectorStart } from '@kbn/inspector-plugin/public'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { LensByReferenceInput, LensEmbeddableInput } from './embeddable'; -import type { Document } from '../persistence/saved_object_store'; -import type { LensAttributeService } from '../lens_attribute_service'; -import { DOC_TYPE } from '../../common/constants'; -import { extract, inject } from '../../common/embeddable_factory'; -import type { DatasourceMap, VisualizationMap } from '../types'; -import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame'; - -export interface LensEmbeddableStartServices { - data: DataPublicPluginStart; - timefilter: TimefilterContract; - coreHttp: HttpSetup; - coreStart: CoreStart; - inspector: InspectorStart; - attributeService: LensAttributeService; - capabilities: RecursiveReadonly<Capabilities>; - expressionRenderer: ReactExpressionRendererType; - dataViews: DataViewsContract; - uiActions?: UiActionsStart; - usageCollection?: UsageCollectionSetup; - documentToExpression: (doc: Document) => Promise<DocumentToExpressionReturnType>; - injectFilterReferences: FilterManager['inject']; - visualizationMap: VisualizationMap; - datasourceMap: DatasourceMap; - spaces?: SpacesPluginStart; - theme: ThemeServiceStart; - uiSettings: IUiSettingsClient; -} - -export class EmbeddableFactory implements EmbeddableFactoryDefinition { - type = DOC_TYPE; - savedObjectMetaData = { - name: i18n.translate('xpack.lens.lensSavedObjectLabel', { - defaultMessage: 'Lens Visualization', - }), - type: DOC_TYPE, - getIconForSavedObject: () => 'lensApp', - }; - - constructor(private getStartServices: () => Promise<LensEmbeddableStartServices>) {} - - public isEditable = async () => { - const { capabilities } = await this.getStartServices(); - return Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls); - }; - - canCreateNew() { - return false; - } - - getDisplayName() { - return i18n.translate('xpack.lens.embeddableDisplayName', { - defaultMessage: 'Lens', - }); - } - - createFromSavedObject = async ( - savedObjectId: string, - input: LensEmbeddableInput, - parent?: IContainer - ) => { - if (!(input as LensByReferenceInput).savedObjectId) { - (input as LensByReferenceInput).savedObjectId = savedObjectId; - } - return this.create(input, parent); - }; - - async create(input: LensEmbeddableInput, parent?: IContainer) { - try { - const { - data, - timefilter, - expressionRenderer, - documentToExpression, - injectFilterReferences, - visualizationMap, - datasourceMap, - uiActions, - coreHttp, - coreStart, - attributeService, - dataViews, - capabilities, - usageCollection, - inspector, - spaces, - uiSettings, - } = await this.getStartServices(); - - const { Embeddable } = await import('../async_services'); - - return new Embeddable( - { - attributeService, - data, - dataViews, - timefilter, - inspector, - expressionRenderer, - basePath: coreHttp.basePath, - getTrigger: uiActions?.getTrigger, - getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, - documentToExpression, - injectFilterReferences, - visualizationMap, - datasourceMap, - capabilities: { - canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), - canSaveVisualizations: Boolean(capabilities.visualize.save), - canOpenVisualizations: Boolean(capabilities.visualize.show), - navLinks: capabilities.navLinks, - discover: capabilities.discover, - }, - coreStart, - usageCollection, - spaces, - uiSettings, - }, - input, - parent - ); - } catch (e) { - return new ErrorEmbeddable(e, input, parent); - } - } - - extract = extract; - inject = inject; -} diff --git a/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts b/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts deleted file mode 100644 index 11b70cd6e7763..0000000000000 --- a/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts +++ /dev/null @@ -1,45 +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 { - HasParentApi, - HasType, - PublishesUnifiedSearch, - PublishesPanelTitle, - PublishingSubject, -} from '@kbn/presentation-publishing'; -import { - apiIsOfType, - apiPublishesUnifiedSearch, - apiPublishesPanelTitle, -} from '@kbn/presentation-publishing'; -import { LensSavedObjectAttributes, ViewUnderlyingDataArgs } from '../embeddable'; - -export type HasLensConfig = HasType<'lens'> & { - getSavedVis: () => Readonly<LensSavedObjectAttributes | undefined>; - canViewUnderlyingData$: PublishingSubject<boolean>; - getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs; - getFullAttributes: () => LensSavedObjectAttributes | undefined; -}; - -export type LensApi = HasLensConfig & - PublishesPanelTitle & - PublishesUnifiedSearch & - Partial<HasParentApi<Partial<PublishesUnifiedSearch>>>; - -export const isLensApi = (api: unknown): api is LensApi => { - return Boolean( - api && - apiIsOfType(api, 'lens') && - typeof (api as HasLensConfig).getSavedVis === 'function' && - (api as HasLensConfig).canViewUnderlyingData$ && - typeof (api as HasLensConfig).getViewUnderlyingDataArgs === 'function' && - typeof (api as HasLensConfig).getFullAttributes === 'function' && - apiPublishesPanelTitle(api) && - apiPublishesUnifiedSearch(api) - ); -}; diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 026da7988a303..aea728024b574 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -7,12 +7,21 @@ import { LensPlugin } from './plugin'; -export { isLensApi } from './embeddable/interfaces/lens_api'; +export { isLensApi } from './react_embeddable/type_guards'; +export { type EmbeddableComponent } from './react_embeddable/renderer/lens_custom_renderer_component'; export type { - EmbeddableComponentProps, - EmbeddableComponent, + LensApi, + LensSerializedState, + LensRuntimeState, + LensByValueInput, + LensByReferenceInput, TypedLensByValueInput, -} from './embeddable/embeddable_component'; + LensEmbeddableInput, + LensEmbeddableOutput, + LensSavedObjectAttributes, + LensRendererProps as EmbeddableComponentProps, +} from './react_embeddable/types'; + export type { XYState, XYReferenceLineLayerConfig, @@ -110,14 +119,6 @@ export type { export type { InlineEditLensEmbeddableContext } from './trigger_actions/open_lens_config/in_app_embeddable_edit/types'; -export type { - LensApi, - LensEmbeddableInput, - LensSavedObjectAttributes, - Embeddable, - LensEmbeddableOutput, -} from './embeddable'; - export type { ChartInfo } from './chart_info_api'; export { layerTypes } from '../common/layer_types'; diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index eb827d87d6416..b5eeaae5d0f54 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -6,27 +6,52 @@ */ import type { CoreStart } from '@kbn/core/public'; -import type { AttributeService } from '@kbn/embeddable-plugin/public'; +import type { SavedObjectReference } from '@kbn/core/types'; import { OnSaveProps } from '@kbn/saved-objects-plugin/public'; import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; +import { noop } from 'lodash'; +import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import type { LensPluginStartDependencies } from './plugin'; -import type { LensSavedObjectAttributes as LensSavedObjectAttributesWithoutReferences } from '../common/content_management'; import type { - LensSavedObjectAttributes, - LensByValueInput, - LensUnwrapMetaInfo, - LensUnwrapResult, - LensByReferenceInput, -} from './embeddable/embeddable'; + LensSavedObject, + LensSavedObjectAttributes as LensSavedObjectAttributesWithoutReferences, +} from '../common/content_management'; +import { extract, inject } from '../common/embeddable_factory'; import { SavedObjectIndexStore, checkForDuplicateTitle } from './persistence'; import { DOC_TYPE } from '../common/constants'; +import { SharingSavedObjectProps } from './types'; +import { LensRuntimeState, LensSavedObjectAttributes } from './react_embeddable/types'; -export type LensAttributeService = AttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput, - LensUnwrapMetaInfo ->; +type Reference = LensSavedObject['references'][number]; + +type CheckDuplicateTitleProps = OnSaveProps & { + id?: string; + displayName: string; + lastSavedTitle: string; + copyOnSave: boolean; +}; + +export interface LensAttributesService { + loadFromLibrary: (savedObjectId: string) => Promise<{ + attributes: LensSavedObjectAttributes; + sharingSavedObjectProps: SharingSavedObjectProps; + managed: boolean; + }>; + saveToLibrary: ( + attributes: LensSavedObjectAttributesWithoutReferences, + references: Reference[], + savedObjectId?: string + ) => Promise<string>; + checkForDuplicateTitle: (props: CheckDuplicateTitleProps) => Promise<{ isDuplicate: boolean }>; + injectReferences: ( + runtimeState: LensRuntimeState, + references: SavedObjectReference[] | undefined + ) => LensRuntimeState; + extractReferences: (runtimeState: LensRuntimeState) => { + rawState: LensRuntimeState; + references: SavedObjectReference[]; + }; +} export const savedObjectToEmbeddableAttributes = ( savedObject: SavedObjectCommon<LensSavedObjectAttributesWithoutReferences> @@ -41,60 +66,86 @@ export const savedObjectToEmbeddableAttributes = ( export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies -): LensAttributeService { +): LensAttributesService { const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement); - return startDependencies.embeddable.getAttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput, - LensUnwrapMetaInfo - >(DOC_TYPE, { - saveMethod: async (attributes: LensSavedObjectAttributes, savedObjectId?: string) => { - const savedDoc = await savedObjectStore.save({ + return { + loadFromLibrary: async ( + savedObjectId: string + ): Promise<{ + attributes: LensSavedObjectAttributes; + sharingSavedObjectProps: SharingSavedObjectProps; + managed: boolean; + }> => { + const { meta, item } = await savedObjectStore.load(savedObjectId); + return { + attributes: { + ...item.attributes, + state: item.attributes.state as LensSavedObjectAttributes['state'], + references: item.references, + }, + sharingSavedObjectProps: { + aliasTargetId: meta.aliasTargetId, + outcome: meta.outcome, + aliasPurpose: meta.aliasPurpose, + sourceId: item.id, + }, + managed: Boolean(item.managed), + }; + }, + saveToLibrary: async ( + attributes: LensSavedObjectAttributesWithoutReferences, + references: Reference[], + savedObjectId?: string + ) => { + const result = await savedObjectStore.save({ ...attributes, + state: attributes.state as LensSavedObjectAttributes['state'], + references, savedObjectId, - type: DOC_TYPE, }); - return { id: savedDoc.savedObjectId }; + return result.savedObjectId; }, - unwrapMethod: async (savedObjectId: string): Promise<LensUnwrapResult> => { - const { - item: savedObject, - meta: { outcome, aliasTargetId, aliasPurpose }, - } = await savedObjectStore.load(savedObjectId); - const { id } = savedObject; - - const sharingSavedObjectProps = { - aliasTargetId, - outcome, - aliasPurpose, - sourceId: id, - }; - + checkForDuplicateTitle: async ({ + newTitle, + isTitleDuplicateConfirmed, + onTitleDuplicate = noop, + displayName = DOC_TYPE, + lastSavedTitle = '', + copyOnSave = false, + id, + }: CheckDuplicateTitleProps) => { return { - attributes: savedObjectToEmbeddableAttributes(savedObject), - metaInfo: { - sharingSavedObjectProps, - managed: savedObject.managed, - }, + isDuplicate: await checkForDuplicateTitle( + { + id, + title: newTitle, + isTitleDuplicateConfirmed, + displayName, + lastSavedTitle, + copyOnSave, + }, + onTitleDuplicate, + { + client: savedObjectStore, + ...core, + } + ), }; }, - checkForDuplicateTitle: (props: OnSaveProps) => { - return checkForDuplicateTitle( - { - title: props.newTitle, - displayName: DOC_TYPE, - isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed, - lastSavedTitle: '', - copyOnSave: false, - }, - props.onTitleDuplicate, - { - client: savedObjectStore, - ...core, - } - ); + // Make sure to inject references from the container down to the runtime state + // this ensure migrations/copy to spaces works correctly + injectReferences: (runtimeState, references) => { + return inject( + runtimeState as unknown as EmbeddableStateWithType, + references ?? runtimeState.attributes.references + ) as unknown as LensRuntimeState; }, - }); + // Make sure to move the internal references into the parent references + // so migrations/move to spaces can work properly + extractReferences: (runtimeState) => { + const { state, references } = extract(runtimeState as unknown as EmbeddableStateWithType); + return { rawState: state as unknown as LensRuntimeState, references }; + }, + }; } diff --git a/x-pack/plugins/lens/public/lens_inspector_service.ts b/x-pack/plugins/lens/public/lens_inspector_service.ts index 4de0a8ec1340f..052a741851ba9 100644 --- a/x-pack/plugins/lens/public/lens_inspector_service.ts +++ b/x-pack/plugins/lens/public/lens_inspector_service.ts @@ -18,7 +18,7 @@ export const getLensInspectorService = (inspector: InspectorStartContract) => { const adapters: Adapters = createDefaultInspectorAdapters(); let overlayRef: InspectorSession | undefined; return { - adapters, + getInspectorAdapters: () => adapters, inspect: (options?: InspectorOptions) => { overlayRef = inspector.open(adapters, options); overlayRef.onClose.then(() => { @@ -28,7 +28,7 @@ export const getLensInspectorService = (inspector: InspectorStartContract) => { }); return overlayRef; }, - close: () => overlayRef?.close(), + closeInspector: async () => overlayRef?.close(), }; }; diff --git a/x-pack/plugins/lens/public/lens_suggestions_api/helpers.test.ts b/x-pack/plugins/lens/public/lens_suggestions_api/helpers.test.ts index 177a7e2e0d33c..fa53ec84293ca 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api/helpers.test.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api/helpers.test.ts @@ -7,7 +7,7 @@ import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { mergeSuggestionWithVisContext } from './helpers'; import { mockAllSuggestions } from '../mocks'; -import type { TypedLensByValueInput } from '../embeddable/embeddable_component'; +import { TypedLensByValueInput } from '../react_embeddable/types'; const context = { dataViewSpec: { diff --git a/x-pack/plugins/lens/public/lens_suggestions_api/helpers.ts b/x-pack/plugins/lens/public/lens_suggestions_api/helpers.ts index 394d32e8c5bb7..5e000d1f14c8a 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api/helpers.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api/helpers.ts @@ -7,7 +7,7 @@ import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { getDatasourceId } from '@kbn/visualization-utils'; import type { VisualizeEditorContext, Suggestion } from '../types'; -import type { TypedLensByValueInput } from '../embeddable/embeddable_component'; +import { TypedLensByValueInput } from '../react_embeddable/types'; /** * Returns the suggestion updated with external visualization state for ES|QL charts diff --git a/x-pack/plugins/lens/public/lens_suggestions_api/index.ts b/x-pack/plugins/lens/public/lens_suggestions_api/index.ts index c73379d9a42cd..6f3f558b60b15 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api/index.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api/index.ts @@ -10,7 +10,7 @@ import type { ChartType } from '@kbn/visualization-utils'; import { getSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers'; import type { DatasourceMap, VisualizationMap, VisualizeEditorContext } from '../types'; import type { DataViewsState } from '../state_management'; -import type { TypedLensByValueInput } from '../embeddable/embeddable_component'; +import type { TypedLensByValueInput } from '../react_embeddable/types'; import { mergeSuggestionWithVisContext } from './helpers'; interface SuggestionsApiProps { diff --git a/x-pack/plugins/lens/public/lens_suggestions_api/lens_suggestions_api.test.ts b/x-pack/plugins/lens/public/lens_suggestions_api/lens_suggestions_api.test.ts index e5e60284e4919..784c0ae03e56f 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api/lens_suggestions_api.test.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api/lens_suggestions_api.test.ts @@ -10,7 +10,7 @@ import { ChartType } from '@kbn/visualization-utils'; import { createMockVisualization, DatasourceMock, createMockDatasource } from '../mocks'; import { DatasourceSuggestion } from '../types'; import { suggestionsApi } from '.'; -import type { TypedLensByValueInput } from '../embeddable/embeddable_component'; +import { TypedLensByValueInput } from '../react_embeddable/types'; const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({ state, diff --git a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts index db7ab00de22e3..8628cc29c1940 100644 --- a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts @@ -48,13 +48,13 @@ export function mockDataPlugin( function createMockSearchService() { let sessionIdCounter = initialSessionId ? 1 : 0; let currentSessionId: string | undefined = initialSessionId; - const start = () => { - currentSessionId = `sessionId-${++sessionIdCounter}`; - return currentSessionId; - }; + return { session: { - start: jest.fn(start), + start: jest.fn(() => { + currentSessionId = `sessionId-${++sessionIdCounter}`; + return currentSessionId; + }), clear: jest.fn(), getSessionId: jest.fn(() => currentSessionId), getSession$: jest.fn(() => sessionIdSubject.asObservable()), @@ -146,5 +146,6 @@ export function mockDataPlugin( fieldFormats: { deserialize: jest.fn(), }, + datatableUtilities: { getDateHistogramMeta: jest.fn(() => true) }, } as unknown as DataPublicPluginStart; } diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx index 4e5f83c7db839..cbb2f0c5dddbf 100644 --- a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx @@ -16,7 +16,7 @@ type Start = jest.Mocked<LensPublicStart>; export const lensPluginMock = { createStartContract: (): Start => { const startContract: Start = { - EmbeddableComponent: jest.fn(() => { + EmbeddableComponent: jest.fn((props) => { return <span>Lens Embeddable Component</span>; }), SaveModalComponent: jest.fn(() => { diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index 18fa29fd6caf2..b5366984c4352 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import React from 'react'; import { Subject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; @@ -20,46 +19,35 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { - mockAttributeService, - createEmbeddableStateTransferMock, -} from '@kbn/embeddable-plugin/public/mocks'; +import { createEmbeddableStateTransferMock } from '@kbn/embeddable-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import type { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; -import type { LensAttributeService } from '../lens_attribute_service'; -import type { - LensByValueInput, - LensByReferenceInput, - LensSavedObjectAttributes, - LensUnwrapMetaInfo, -} from '../embeddable/embeddable'; -import { DOC_TYPE } from '../../common/constants'; + import { LensAppServices } from '../app_plugin/types'; import { mockDataPlugin } from './data_plugin_mock'; import { getLensInspectorService } from '../lens_inspector_service'; -import { SavedObjectIndexStore } from '../persistence'; +import { LensDocument, SavedObjectIndexStore } from '../persistence'; +import { LensAttributesService } from '../lens_attribute_service'; +import { mockDatasourceStates } from './store_mocks'; const startMock = coreMock.createStart(); -export const defaultDoc = { +export const defaultDoc: LensDocument = { savedObjectId: '1234', title: 'An extremely cool default document!', - expression: 'definitely a valid expression', visualizationType: 'testVis', state: { - query: 'kuery', + query: { query: 'test', language: 'kuery' }, filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }], - datasourceStates: { - testDatasource: 'datasource', - }, + datasourceStates: mockDatasourceStates(), visualization: {}, }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], -} as unknown as Document; +}; export const exactMatchDoc = { attributes: { @@ -70,6 +58,27 @@ export const exactMatchDoc = { }, }; +export function makeAttributeService(doc: LensDocument): jest.Mocked<LensAttributesService> { + const attributeServiceMock: jest.Mocked<LensAttributesService> = { + loadFromLibrary: jest.fn().mockResolvedValue(exactMatchDoc), + saveToLibrary: jest.fn().mockResolvedValue(doc.savedObjectId), + checkForDuplicateTitle: jest.fn(), + injectReferences: jest.fn((_runtimeState, references) => ({ + ..._runtimeState, + attributes: { + ..._runtimeState.attributes, + references: references?.length ? references : _runtimeState.attributes.references, + }, + })), + extractReferences: jest.fn((_runtimeState) => ({ + rawState: _runtimeState, + references: _runtimeState.attributes.references || [], + })), + }; + + return attributeServiceMock; +} + export function makeDefaultServices( sessionIdSubject = new Subject<string>(), sessionId: string | undefined = undefined, @@ -106,44 +115,16 @@ export function makeDefaultServices( const navigationStartMock = navigationPluginMock.createStartContract(); - jest - .spyOn(navigationStartMock.ui.AggregateQueryTopNavMenu.prototype, 'constructor') - .mockImplementation(() => { - return <div className="topNavMenu" />; - }); - - function makeAttributeService(): LensAttributeService { - const attributeServiceMock = mockAttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput, - LensUnwrapMetaInfo - >( - DOC_TYPE, - { - saveMethod: jest.fn(), - unwrapMethod: jest.fn(), - checkForDuplicateTitle: jest.fn(), - }, - core - ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); - attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ - savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, - }); - - return attributeServiceMock; - } - return { ...startMock, chrome: core.chrome, navigation: navigationStartMock, - attributeService: makeAttributeService(), + attributeService: makeAttributeService(doc), inspector: { - adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters, + getInspectorAdapters: getLensInspectorService(inspectorPluginMock.createStartContract()) + .getInspectorAdapters, inspect: jest.fn(), - close: jest.fn(), + closeInspector: jest.fn(), }, presentationUtil: presentationUtilPluginMock.createStartContract(), savedObjectStore: { @@ -158,6 +139,9 @@ export function makeDefaultServices( capabilities: { ...core.application.capabilities, visualize: { save: true, saveQuery: true, show: true, createShortUrl: true }, + dashboard: { + showWriteControls: true, + }, }, getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), }, diff --git a/x-pack/plugins/lens/public/mocks/store_mocks.tsx b/x-pack/plugins/lens/public/mocks/store_mocks.tsx index f465eadc9dfdd..87667c21fed20 100644 --- a/x-pack/plugins/lens/public/mocks/store_mocks.tsx +++ b/x-pack/plugins/lens/public/mocks/store_mocks.tsx @@ -8,7 +8,6 @@ import React, { PropsWithChildren, ReactElement } from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { Provider } from 'react-redux'; -import { act } from 'react-dom/test-utils'; import { PreloadedState } from '@reduxjs/toolkit'; import { RenderOptions, render } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; @@ -20,17 +19,25 @@ import { mockVisualizationMap } from './visualization_mock'; import { mockDatasourceMap } from './datasource_mock'; import { makeDefaultServices } from './services_mock'; -export const mockStoreDeps = (deps?: { - lensServices?: LensAppServices; - datasourceMap?: DatasourceMap; - visualizationMap?: VisualizationMap; -}) => { - return { - datasourceMap: deps?.datasourceMap || mockDatasourceMap(), - visualizationMap: deps?.visualizationMap || mockVisualizationMap(), - lensServices: deps?.lensServices || makeDefaultServices(), - }; -}; +export const mockStoreDeps = ( + { + lensServices = makeDefaultServices(), + datasourceMap = mockDatasourceMap(), + visualizationMap = mockVisualizationMap(), + }: { + lensServices?: LensAppServices; + datasourceMap?: DatasourceMap; + visualizationMap?: VisualizationMap; + } = { + lensServices: makeDefaultServices(), + datasourceMap: mockDatasourceMap(), + visualizationMap: mockVisualizationMap(), + } +) => ({ + datasourceMap, + visualizationMap, + lensServices, +}); export function mockDatasourceStates() { return { @@ -138,12 +145,7 @@ export const mountWithProvider = async ( } ) => { const { mountArgs, lensStore, deps } = getMountWithProviderParams(component, store, options); - - let instance: ReactWrapper = {} as ReactWrapper; - - await act(async () => { - instance = mount(mountArgs.component, mountArgs.options); - }); + const instance = mount(mountArgs.component, mountArgs.options); return { instance, lensStore, deps }; }; diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index d15386548dacf..9edd481f7b62f 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Filter, Query } from '@kbn/es-query'; -import { SavedObjectReference } from '@kbn/core/public'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import type { SavedObjectReference } from '@kbn/core/public'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { SearchQuery } from '@kbn/content-management-plugin/common'; @@ -14,7 +14,7 @@ import type { VisualizationClient } from '@kbn/visualizations-plugin/public'; import type { LensSavedObjectAttributes, LensSearchQuery } from '../../common/content_management'; import { getLensClient } from './lens_client'; -export interface Document { +export interface LensDocument { savedObjectId?: string; type?: string; visualizationType: string | null; @@ -23,7 +23,7 @@ export interface Document { state: { datasourceStates: Record<string, unknown>; visualization: unknown; - query: Query; + query: Query | AggregateQuery; globalPalette?: { activePaletteId: string; state?: unknown; @@ -36,7 +36,7 @@ export interface Document { } export interface DocumentSaver { - save: (vis: Document) => Promise<{ savedObjectId: string }>; + save: (vis: LensDocument) => Promise<{ savedObjectId: string }>; } export interface DocumentLoader { @@ -52,9 +52,8 @@ export class SavedObjectIndexStore implements SavedObjectStore { this.client = getLensClient(cm); } - save = async (vis: Document) => { - const { savedObjectId, type, references, ...rest } = vis; - const attributes = rest; + save = async (vis: LensDocument) => { + const { savedObjectId, type, references, ...attributes } = vis; if (savedObjectId) { const result = await this.client.update({ @@ -65,15 +64,14 @@ export class SavedObjectIndexStore implements SavedObjectStore { }, }); return { ...vis, savedObjectId: result.item.id }; - } else { - const result = await this.client.create({ - data: attributes, - options: { - references, - }, - }); - return { ...vis, savedObjectId: result.item.id }; } + const result = await this.client.create({ + data: attributes, + options: { + references, + }, + }); + return { ...vis, savedObjectId: result.item.id }; }; async load(savedObjectId: string) { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 3145606abaf6c..38f831ce34151 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -14,8 +14,9 @@ import type { } from '@kbn/usage-collection-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; +import type { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { @@ -24,6 +25,7 @@ import type { ExpressionsStart, } from '@kbn/expressions-plugin/public'; import { + ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS, DASHBOARD_VISUALIZATION_PANEL_TRIGGER, VisualizationsSetup, VisualizationsStart, @@ -94,7 +96,13 @@ import type { HeatmapVisualization as HeatmapVisualizationType } from './visuali import type { GaugeVisualization as GaugeVisualizationType } from './visualizations/gauge'; import type { TagcloudVisualization as TagcloudVisualizationType } from './visualizations/tagcloud'; -import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common/constants'; +import { + APP_ID, + getEditPath, + LENS_EMBEDDABLE_TYPE, + LENS_ICON, + NOT_INTERNATIONALIZED_PRODUCT_NAME, +} from '../common/constants'; import type { FormatFactory } from '../common/types'; import type { Visualization, @@ -103,10 +111,11 @@ import type { LensTopNavMenuEntryGenerator, VisualizeEditorContext, Suggestion, + DatasourceMap, + VisualizationMap, } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action'; -import { ConfigureInLensPanelAction } from './trigger_actions/open_lens_config/edit_action'; import { CreateESQLPanelAction } from './trigger_actions/open_lens_config/create_action'; import { inAppEmbeddableEditTrigger, @@ -115,12 +124,12 @@ import { import { EditLensEmbeddableAction } from './trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { visualizeTSVBAction } from './trigger_actions/visualize_tsvb_actions'; -import { visualizeAggBasedVisAction } from './trigger_actions/visualize_agg_based_vis_actions'; -import { visualizeDashboardVisualizePanelction } from './trigger_actions/dashboard_visualize_panel_actions'; -import type { LensByValueInput, LensEmbeddableInput } from './embeddable'; -import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory'; -import { EmbeddableComponent, getEmbeddableComponent } from './embeddable/embeddable_component'; +import type { + LensEmbeddableStartServices, + LensSerializedState, + TypedLensByValueInput, +} from './react_embeddable/types'; import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy'; import type { SaveModalContainerProps } from './app_plugin/save_modal_container'; @@ -130,15 +139,16 @@ import { OpenInDiscoverDrilldown } from './trigger_actions/open_in_discover_dril import { ChartInfoApi } from './chart_info_api'; import { type LensAppLocator, LensAppLocatorDefinition } from '../common/locator/locator'; import { downloadCsvShareProvider } from './app_plugin/csv_download_provider/csv_download_provider'; - +import { LensDocument } from './persistence/saved_object_store'; import { CONTENT_ID, LATEST_VERSION, LensSavedObjectAttributes, } from '../common/content_management'; import type { EditLensConfigurationProps } from './app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration'; -import { savedObjectToEmbeddableAttributes } from './lens_attribute_service'; -import type { TypedLensByValueInput } from './embeddable/embeddable_component'; +import { convertToLensActionFactory } from './trigger_actions/convert_to_lens_action'; +import { LensRenderer } from './react_embeddable/renderer/lens_custom_renderer_component'; +import { deserializeState } from './react_embeddable/helper'; export type { SaveProps } from './app_plugin'; @@ -182,6 +192,7 @@ export interface LensPluginStartDependencies { contentManagement: ContentManagementPublicStart; serverless?: ServerlessPluginStart; licensing?: LicensingPluginStart; + embeddableEnhanced?: EmbeddableEnhancedPluginStart; } export interface LensPublicSetup { @@ -221,7 +232,7 @@ export interface LensPublicStart { * * @experimental */ - EmbeddableComponent: EmbeddableComponent; + EmbeddableComponent: typeof LensRenderer; /** * React component which can be used to embed a Lens Visualization Save Modal Component. * See `x-pack/examples/embedded_lens_example` for exemplary usage. @@ -248,7 +259,7 @@ export interface LensPublicStart { * @experimental */ navigateToPrefilledEditor: ( - input: LensEmbeddableInput | undefined, + input: LensSerializedState | undefined, options?: { openInNewTab?: boolean; originatingApp?: string; @@ -303,9 +314,14 @@ export class LensPlugin { private topNavMenuEntries: LensTopNavMenuEntryGenerator[] = []; private hasDiscoverAccess: boolean = false; private dataViewsService: DataViewsPublicPluginStart | undefined; - private initDependenciesForApi: () => void = () => {}; private locator?: LensAppLocator; + // Note: this method will be overwritten in the setup flow + private initEditorFrameService = async (): Promise<{ + datasourceMap: DatasourceMap; + visualizationMap: VisualizationMap; + }> => ({ datasourceMap: {}, visualizationMap: {} }); + setup( core: CoreSetup<LensPluginStartDependencies, void>, { @@ -326,26 +342,16 @@ export class LensPlugin { const startServices = createStartServicesGetter(core.getStartServices); const getStartServicesForEmbeddable = async (): Promise<LensEmbeddableStartServices> => { - const { getLensAttributeService, setUsageCollectionStart, initMemoizedErrorNotification } = - await import('./async_services'); + const { setUsageCollectionStart, initMemoizedErrorNotification } = await import( + './async_services' + ); const { core: coreStart, plugins } = startServices(); - await this.initParts( - core, - data, - charts, - expressions, - fieldFormats, - plugins.fieldFormats.deserialize - ); - const [visualizationMap, datasourceMap] = await Promise.all([ - this.editorFrameService!.loadVisualizations(), - this.editorFrameService!.loadDatasources(), + const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); + const [{ getLensAttributeService }, eventAnnotationService] = await Promise.all([ + import('./async_services'), + plugins.eventAnnotation.getService(), ]); - const { setVisualizationMap, setDatasourceMap } = await import('./async_services'); - setDatasourceMap(datasourceMap); - setVisualizationMap(visualizationMap); - const eventAnnotationService = await plugins.eventAnnotation.getService(); if (plugins.usageCollection) { setUsageCollectionStart(plugins.usageCollection); @@ -354,14 +360,14 @@ export class LensPlugin { initMemoizedErrorNotification(coreStart); return { + ...plugins, attributeService: getLensAttributeService(coreStart, plugins), capabilities: coreStart.application.capabilities, coreHttp: coreStart.http, coreStart, - data: plugins.data, timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, - documentToExpression: (doc) => + documentToExpression: (doc: LensDocument) => this.editorFrameService!.documentToExpression(doc, { dataViews: plugins.dataViews, storage: new Storage(localStorage), @@ -373,36 +379,45 @@ export class LensPlugin { injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), visualizationMap, datasourceMap, - dataViews: plugins.dataViews, - uiActions: plugins.uiActions, - usageCollection, - inspector: plugins.inspector, - spaces: plugins.spaces, theme: core.theme, uiSettings: core.uiSettings, }; }; if (embeddable) { - embeddable.registerEmbeddableFactory( - 'lens', - new EmbeddableFactory(getStartServicesForEmbeddable) - ); - - embeddable.registerSavedObjectToPanelMethod<LensSavedObjectAttributes, LensByValueInput>( - CONTENT_ID, - (savedObject) => { - if (!savedObject.managed) { - return { savedObjectId: savedObject.id }; - } - - const panel = { - attributes: savedObjectToEmbeddableAttributes(savedObject), - }; - - return panel; - } - ); + // Let Kibana know about the Lens embeddable + embeddable.registerReactEmbeddableFactory(LENS_EMBEDDABLE_TYPE, async () => { + const [deps, { createLensEmbeddableFactory }] = await Promise.all([ + getStartServicesForEmbeddable(), + import('./react_embeddable/lens_embeddable'), + ]); + return createLensEmbeddableFactory(deps); + }); + + // Let Dashboard know about the Lens panel type + embeddable.registerReactEmbeddableSavedObject<LensSavedObjectAttributes>({ + onAdd: async (container, savedObject) => { + const { attributeService } = await getStartServicesForEmbeddable(); + // deserialize the saved object from visualize library + // this make sure to fit into the new embeddable model, where the following build() + // function expects a fully loaded runtime state + const state = await deserializeState( + attributeService, + { savedObjectId: savedObject.id }, + savedObject.references + ); + container.addNewPanel({ + panelType: LENS_EMBEDDABLE_TYPE, + initialState: state, + }); + }, + embeddableType: LENS_EMBEDDABLE_TYPE, + savedObjectType: LENS_EMBEDDABLE_TYPE, + savedObjectName: i18n.translate('xpack.lens.mapSavedObjectLabel', { + defaultMessage: 'Lens', + }), + getIconForSavedObject: () => LENS_ICON, + }); } if (share) { @@ -509,9 +524,10 @@ export class LensPlugin { ); } - urlForwarding.forwardApp('lens', 'lens'); + urlForwarding.forwardApp(APP_ID, APP_ID); - this.initDependenciesForApi = async () => { + // Note: this overwrites a method defined above + this.initEditorFrameService = async () => { const { plugins } = startServices(); await this.initParts( core, @@ -521,6 +537,15 @@ export class LensPlugin { fieldFormats, plugins.fieldFormats.deserialize ); + // This needs to be executed before the import call to avoid race conditions + const [visualizationMap, datasourceMap] = await Promise.all([ + this.editorFrameService!.loadVisualizations(), + this.editorFrameService!.loadDatasources(), + ]); + const { setVisualizationMap, setDatasourceMap } = await import('./async_services'); + setDatasourceMap(datasourceMap); + setVisualizationMap(visualizationMap); + return { datasourceMap, visualizationMap }; }; return { @@ -625,21 +650,33 @@ export class LensPlugin { startDependencies.uiActions.addTriggerAction( DASHBOARD_VISUALIZATION_PANEL_TRIGGER, - visualizeDashboardVisualizePanelction(core.application) + convertToLensActionFactory( + ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS, + i18n.translate('xpack.lens.visualizeLegacyVisualizationChart', { + defaultMessage: 'Visualize legacy visualization chart', + }), + i18n.translate('xpack.lens.dashboardLabel', { + defaultMessage: 'Dashboard', + }) + )(core.application) ); startDependencies.uiActions.addTriggerAction( AGG_BASED_VISUALIZATION_TRIGGER, - visualizeAggBasedVisAction(core.application) + convertToLensActionFactory( + ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS, + i18n.translate('xpack.lens.visualizeAggBasedLegend', { + defaultMessage: 'Visualize agg based chart', + }), + i18n.translate('xpack.lens.AggBasedLabel', { + defaultMessage: 'aggregation based visualization', + }) + )(core.application) ); - const editInLensAction = new ConfigureInLensPanelAction(startDependencies, core); - // dashboard edit panel action - startDependencies.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction); - - // Allows the Lens embeddable to easily open the inapp editing flyout + // Allows the Lens embeddable to easily open the inline editing flyout const editLensEmbeddableAction = new EditLensEmbeddableAction(startDependencies, core); - // embeddable edit panel action + // embeddable inline edit panel action startDependencies.uiActions.addTriggerAction( IN_APP_EMBEDDABLE_EDIT_TRIGGER, editLensEmbeddableAction @@ -648,7 +685,7 @@ export class LensPlugin { // Displays the add ESQL panel in the dashboard add Panel menu const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core, async () => { if (!this.editorFrameService) { - await this.initDependenciesForApi(); + await this.initEditorFrameService(); } return this.editorFrameService!; @@ -668,7 +705,7 @@ export class LensPlugin { } return { - EmbeddableComponent: getEmbeddableComponent(core, startDependencies), + EmbeddableComponent: LensRenderer, SaveModalComponent: getSaveModalComponent(core, startDependencies), navigateToPrefilledEditor: ( input, @@ -705,16 +742,15 @@ export class LensPlugin { const { createFormulaPublicApi, createChartInfoApi, suggestionsApi } = await import( './async_services' ); - if (!this.editorFrameService) { - await this.initDependenciesForApi(); - } - const [visualizationMap, datasourceMap] = await Promise.all([ - this.editorFrameService!.loadVisualizations(), - this.editorFrameService!.loadDatasources(), - ]); + + const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); return { formula: createFormulaPublicApi(), - chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService), + chartInfo: createChartInfoApi( + startDependencies.dataViews, + visualizationMap, + datasourceMap + ), suggestions: ( context, dataView, @@ -734,15 +770,11 @@ export class LensPlugin { }, }; }, + // TODO: remove this in faviour of the custom action thing + // This is currently used in Discover by the unified histogram plugin EditLensConfigPanelApi: async () => { + const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); const { getEditLensConfiguration } = await import('./async_services'); - if (!this.editorFrameService) { - this.initDependenciesForApi(); - } - const [visualizationMap, datasourceMap] = await Promise.all([ - this.editorFrameService!.loadVisualizations(), - this.editorFrameService!.loadDatasources(), - ]); const Component = await getEditLensConfiguration( core, startDependencies, diff --git a/x-pack/plugins/lens/public/react_embeddable/data_loader.ts b/x-pack/plugins/lens/public/react_embeddable/data_loader.ts new file mode 100644 index 0000000000000..0aed3edf70b89 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/data_loader.ts @@ -0,0 +1,329 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import { fetch$, type FetchContext } from '@kbn/presentation-publishing'; +import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; +import { type KibanaExecutionContext } from '@kbn/core/public'; +import { + BehaviorSubject, + type Subscription, + distinctUntilChanged, + debounceTime, + skip, + pipe, + merge, + tap, + map, +} from 'rxjs'; +import fastIsEqual from 'fast-deep-equal'; +import { getEditPath } from '../../common/constants'; +import type { + GetStateType, + LensApi, + LensInternalApi, + LensPublicCallbacks, + VisualizationContextHelper, +} from './types'; +import { getExpressionRendererParams } from './expressions/expression_params'; +import type { LensEmbeddableStartServices } from './types'; +import { prepareCallbacks } from './expressions/callbacks'; +import { buildUserMessagesHelpers } from './user_messages/api'; +import { getLogError } from './expressions/telemetry'; +import type { SharingSavedObjectProps, UserMessagesDisplayLocationId } from '../types'; +import { apiHasLensComponentCallbacks } from './type_guards'; +import { getRenderMode, getParentContext } from './helper'; +import { addLog } from './logger'; +import { getUsedDataViews } from './expressions/update_data_views'; +import { getMergedSearchContext } from './expressions/merged_search_context'; + +const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [ + 'visualization', + 'visualizationOnEmbeddable', +]; + +type ReloadReason = + | 'attributes' + | 'savedObjectId' + | 'overrides' + | 'disableTriggers' + | 'viewMode' + | 'searchContext'; + +/** + * The function computes the expression used to render the panel and produces the necessary props + * for the ExpressionWrapper component, binding any outer context to them. + * @returns + */ +export function loadEmbeddableData( + uuid: string, + getState: GetStateType, + api: LensApi, + parentApi: unknown, + internalApi: LensInternalApi, + services: LensEmbeddableStartServices, + { getVisualizationContext, updateVisualizationContext }: VisualizationContextHelper, + metaInfo?: SharingSavedObjectProps +) { + const { onLoad, onBeforeBadgesRender, ...callbacks } = apiHasLensComponentCallbacks(parentApi) + ? parentApi + : ({} as LensPublicCallbacks); + + // Some convenience api for the user messaging + const { + getUserMessages, + addUserMessages, + updateBlockingErrors, + updateValidationErrors, + updateWarnings, + resetMessages, + updateMessages, + } = buildUserMessagesHelpers( + api, + internalApi, + getVisualizationContext, + services, + onBeforeBadgesRender, + services.spaces, + metaInfo + ); + + const dispatchBlockingErrorIfAny = () => { + const blockingErrors = getUserMessages(blockingMessageDisplayLocations, { + severity: 'error', + }); + updateValidationErrors(blockingErrors); + updateBlockingErrors(blockingErrors); + if (blockingErrors.length > 0) { + internalApi.dispatchError(); + } + return blockingErrors.length > 0; + }; + + const onRenderComplete = () => { + updateMessages(getUserMessages('embeddableBadge')); + // No issues so far, blocking errors are handled directly by Lens from this point on + if (!dispatchBlockingErrorIfAny()) { + internalApi.dispatchRenderComplete(); + } + }; + + const unifiedSearch$ = new BehaviorSubject< + Pick<FetchContext, 'query' | 'filters' | 'timeRange' | 'timeslice' | 'searchSessionId'> + >({ + query: undefined, + filters: undefined, + timeRange: undefined, + timeslice: undefined, + searchSessionId: undefined, + }); + + async function reload( + // make reload easier to debug + sourceId: ReloadReason + ) { + addLog(`Embeddable reload reason: ${sourceId}`); + resetMessages(); + + // reset the render on reload + internalApi.dispatchRenderStart(); + + // notify about data loading + internalApi.updateDataLoading(true); + + // the component is ready to load + if (apiHasLensComponentCallbacks(parentApi)) { + parentApi.onLoad?.(true); + } + + const currentState = getState(); + + const { searchSessionId, ...unifiedSearch } = unifiedSearch$.getValue(); + + const getExecutionContext = () => { + const parentContext = getParentContext(parentApi); + const lastState = getState(); + if (lastState.attributes) { + const child: KibanaExecutionContext = { + type: 'lens', + name: lastState.attributes.visualizationType ?? '', + id: uuid || 'new', + description: lastState.attributes.title || lastState.title || '', + url: `${services.coreStart.application.getUrlForApp('lens')}${getEditPath( + lastState.savedObjectId + )}`, + }; + + return parentContext + ? { + ...parentContext, + child, + } + : child; + } + }; + + const onDataCallback = (adapters: Partial<DefaultInspectorAdapters> | undefined) => { + updateVisualizationContext({ + activeData: adapters?.tables?.tables, + }); + // data has loaded + internalApi.updateDataLoading(false); + // The third argument here is an observable to let the + // consumer to be notified on data change + onLoad?.(false, adapters, api.dataLoading); + + api.loadViewUnderlyingData(); + + updateWarnings(); + // Render can still go wrong, so perfor a new check + dispatchBlockingErrorIfAny(); + }; + + const { onRender, onData, handleEvent, disableTriggers } = prepareCallbacks( + api, + internalApi, + parentApi, + getState, + services, + getExecutionContext(), + onDataCallback, + onRenderComplete, + callbacks + ); + + const searchContext = getMergedSearchContext( + currentState, + unifiedSearch, + api.timeRange$, + parentApi, + services + ); + + // Go concurrently: build the expression and fetch the dataViews + const [{ params, abortController, ...rest }, dataViews] = await Promise.all([ + getExpressionRendererParams(currentState, { + searchContext, + api, + settings: { + syncColors: currentState.syncColors, + syncCursor: currentState.syncCursor, + syncTooltips: currentState.syncTooltips, + }, + renderMode: getRenderMode(parentApi), + services, + searchSessionId, + abortController: internalApi.expressionAbortController$.getValue(), + getExecutionContext, + logError: getLogError(getExecutionContext), + addUserMessages, + onRender, + onData, + handleEvent, + disableTriggers, + updateBlockingErrors, + renderCount: internalApi.renderCount$.getValue(), + }), + getUsedDataViews( + currentState.attributes.references, + currentState.attributes.state?.adHocDataViews, + services.dataViews + ), + ]); + + // update the visualization context before anything else + // as it will be used to compute blocking errors also in case of issues + updateVisualizationContext({ + doc: currentState.attributes, + mergedSearchContext: params?.searchContext || {}, + ...rest, + }); + + // Publish the used dataViews on the Lens API + internalApi.updateDataViews(dataViews); + + if (params?.expression != null && !dispatchBlockingErrorIfAny()) { + internalApi.updateExpressionParams(params); + } + + internalApi.updateAbortController(abortController); + } + + // Build a custom operator to be resused for various observables + function waitUntilChanged() { + return pipe(distinctUntilChanged(fastIsEqual), skip(1)); + } + + const mergedSubscriptions = merge( + // on data change from the parentApi, reload + fetch$(api).pipe( + tap((data) => { + const searchSessionId = apiPublishesSearchSession(parentApi) ? data.searchSessionId : ''; + unifiedSearch$.next({ + query: data.query, + filters: data.filters, + timeRange: data.timeRange, + timeslice: data.timeslice, + searchSessionId, + }); + }), + map(() => 'searchContext' as ReloadReason) + ), + // On state change, reload + // this is used to refresh the chart on inline editing + // just make sure to avoid to rerender if there's no substantial change + // make sure to debounce one tick to make the refresh work + internalApi.attributes$.pipe( + waitUntilChanged(), + tap(() => { + // the ES|QL query may have changed, so recompute the args for view underlying data + if (api.isTextBasedLanguage()) { + api.loadViewUnderlyingData(); + } + }), + map(() => 'attributes' as ReloadReason) + ), + api.savedObjectId.pipe( + waitUntilChanged(), + map(() => 'savedObjectId' as ReloadReason) + ), + internalApi.overrides$.pipe( + waitUntilChanged(), + map(() => 'overrides' as ReloadReason) + ), + internalApi.disableTriggers$.pipe( + waitUntilChanged(), + map(() => 'disableTriggers' as ReloadReason) + ) + ); + + const subscriptions: Subscription[] = [ + mergedSubscriptions.pipe(debounceTime(0)).subscribe(reload), + // make sure to reload on viewMode change + api.viewMode.subscribe(() => { + // only reload if drilldowns are set + if (getState().enhancements?.dynamicActions) { + reload('viewMode'); + } + }), + ]; + // There are few key moments when errors are checked and displayed: + // * at setup time (here) before the first expression evaluation + // * at runtime => when the expression is running and ES/Kibana server could emit errors) + // * at data time => data has arrived but for something goes wrong + // * at render time => rendering happened but somethign went wrong + // Bubble the error up to the embeddable system if any + dispatchBlockingErrorIfAny(); + + return { + cleanup: () => { + for (const subscription of subscriptions) { + subscription.unsubscribe(); + } + }, + }; +} diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/react_embeddable/expression_wrapper.tsx similarity index 88% rename from x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx rename to x-pack/plugins/lens/public/react_embeddable/expression_wrapper.tsx index d16df5bf9d1e8..e0d21d9ba8356 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/react_embeddable/expression_wrapper.tsx @@ -17,7 +17,7 @@ import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/co import classNames from 'classnames'; import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper'; import { LensInspector } from '../lens_inspector_service'; -import { AddUserMessages } from '../types'; +import { UserMessage } from '../types'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -31,7 +31,7 @@ export interface ExpressionWrapperProps { data: unknown, inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined ) => void; - onRender$: () => void; + onRender$: (count: number) => void; renderMode?: RenderMode; syncColors?: boolean; syncTooltips?: boolean; @@ -40,7 +40,7 @@ export interface ExpressionWrapperProps { getCompatibleCellValueActions?: ReactExpressionRendererProps['getCompatibleCellValueActions']; style?: React.CSSProperties; className?: string; - addUserMessages: AddUserMessages; + addUserMessages: (messages: UserMessage[]) => void; onRuntimeError: (error: Error) => void; executionContext?: KibanaExecutionContext; lensInspector: LensInspector; @@ -75,7 +75,11 @@ export function ExpressionWrapper({ }: ExpressionWrapperProps) { if (!expression) return null; return ( - <div className={classNames('lnsExpressionRenderer', className)} style={style}> + <div + className={classNames('lnsExpressionRenderer', className)} + style={style} + data-test-subj="lens-embeddable" + > <ExpressionRendererComponent className="lnsExpressionRenderer__component" padding={noPadding ? undefined : 's'} @@ -88,7 +92,7 @@ export function ExpressionWrapper({ // @ts-expect-error upgrade typescript v4.9.5 onData$={onData$} onRender$={onRender$} - inspectorAdapters={lensInspector.adapters} + inspectorAdapters={lensInspector.getInspectorAdapters()} renderMode={renderMode} syncColors={syncColors} syncTooltips={syncTooltips} @@ -98,12 +102,7 @@ export function ExpressionWrapper({ renderError={(errorMessage, error) => { const messages = getOriginalRequestErrorMessages(error || null); addUserMessages(messages); - if (error?.original) { - onRuntimeError(error.original); - } else { - onRuntimeError(new Error(errorMessage ? errorMessage : '')); - } - + onRuntimeError(error?.original || new Error(errorMessage ? errorMessage : '')); return <></>; // the embeddable will take care of displaying the messages }} onEvent={handleEvent} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/callbacks.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/callbacks.ts new file mode 100644 index 0000000000000..78a9aa6ab9186 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/callbacks.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 type { KibanaExecutionContext } from '@kbn/core/public'; +import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import { apiHasDisableTriggers } from '@kbn/presentation-publishing'; +import { + GetStateType, + LensApi, + LensEmbeddableStartServices, + LensInternalApi, + LensPublicCallbacks, +} from '../types'; +import { prepareOnRender } from './on_render'; +import { prepareEventHandler } from './on_event'; +import { addLog } from '../logger'; + +export function prepareCallbacks( + api: LensApi, + internalApi: LensInternalApi, + parentApi: unknown, + getState: GetStateType, + services: LensEmbeddableStartServices, + executionContext: KibanaExecutionContext | undefined, + onDataUpdate: (adapters: Partial<DefaultInspectorAdapters | undefined>) => void, + dispatchRenderComplete: () => void, + callbacks: LensPublicCallbacks +) { + const disableTriggers = apiHasDisableTriggers(parentApi) ? parentApi.disableTriggers : undefined; + return { + disableTriggers, + onRender: prepareOnRender( + api, + internalApi, + parentApi, + getState, + services, + executionContext, + dispatchRenderComplete + ), + onData: (_data: unknown, adapters: Partial<DefaultInspectorAdapters> | undefined) => { + addLog(`onData$`); + onDataUpdate(adapters); + }, + handleEvent: prepareEventHandler(api, getState, callbacks, services, disableTriggers), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/expression_params.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/expression_params.ts new file mode 100644 index 0000000000000..e10dded4ad8f9 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/expression_params.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaExecutionContext } from '@kbn/core-execution-context-common'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import { RenderMode } from '@kbn/expressions-plugin/common'; +import { ExpressionRendererEvent } from '@kbn/expressions-plugin/public'; +import { toExpression } from '@kbn/interpreter'; +import { noop } from 'lodash'; +import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; +import { + CellValueContext, + cellValueTrigger, + CELL_VALUE_TRIGGER, +} from '@kbn/embeddable-plugin/public'; +import { DocumentToExpressionReturnType } from '../../async_services'; +import { LensDocument } from '../../persistence'; +import { + GetCompatibleCellValueActions, + IndexPatternMap, + IndexPatternRef, + UserMessage, + isLensFilterEvent, + isLensMultiFilterEvent, + isLensTableRowContextMenuClickEvent, +} from '../../types'; +import type { + ExpressionWrapperProps, + LensApi, + LensEmbeddableStartServices, + LensRuntimeState, +} from '../types'; +import { getVariables } from './variables'; +// import { +// getSearchContextIncompatibleMessage, +// isSearchContextIncompatibleWithDataViews, +// } from '../user_messages/checks'; +import { getExecutionSearchContext, type MergedSearchContext } from './merged_search_context'; + +interface GetExpressionRendererPropsParams { + searchContext: MergedSearchContext; + disableTriggers?: boolean; + renderMode?: RenderMode; + settings: { + syncColors?: boolean; + syncCursor?: boolean; + syncTooltips?: boolean; + }; + services: LensEmbeddableStartServices; + getExecutionContext: () => KibanaExecutionContext | undefined; + searchSessionId?: string; + abortController?: AbortController; + onRender: (count: number) => void; + handleEvent: (event: ExpressionRendererEvent) => void; + onData: ExpressionWrapperProps['onData$']; + logError: (type: 'runtime' | 'validation') => void; + api: LensApi; + addUserMessages: (messages: UserMessage[]) => void; + updateBlockingErrors: (error: Error) => void; + renderCount: number; +} + +async function getExpressionFromDocument( + document: LensDocument, + documentToExpression: (doc: LensDocument) => Promise<DocumentToExpressionReturnType> +) { + const { ast, indexPatterns, indexPatternRefs, activeVisualizationState, activeDatasourceState } = + await documentToExpression(document); + return { + expression: ast ? toExpression(ast) : null, + indexPatterns, + indexPatternRefs, + activeVisualizationState, + activeDatasourceState, + }; +} + +function buildHasCompatibleActions(api: LensApi, { uiActions }: LensEmbeddableStartServices) { + return async (event: ExpressionRendererEvent): Promise<boolean> => { + if (!uiActions?.getTriggerCompatibleActions) { + return false; + } + if ( + isLensTableRowContextMenuClickEvent(event) || + isLensMultiFilterEvent(event) || + isLensFilterEvent(event) + ) { + const actions = await uiActions.getTriggerCompatibleActions( + VIS_EVENT_TO_TRIGGER[event.name], + { + data: event.data, + embeddable: api, + } + ); + + return actions.length > 0; + } + + return false; + }; +} + +function buildGetCompatibleCellValueActions( + api: LensApi, + { uiActions }: LensEmbeddableStartServices +): GetCompatibleCellValueActions { + return async (data) => { + if (!uiActions?.getTriggerCompatibleActions) { + return []; + } + const actions: Array<Action<CellValueContext>> = (await uiActions.getTriggerCompatibleActions( + CELL_VALUE_TRIGGER, + { data, embeddable: api } + )) as Array<Action<CellValueContext>>; + return actions + .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) + .map((action) => ({ + id: action.id, + type: action.type, + iconType: action.getIconType({ embeddable: api, data, trigger: cellValueTrigger })!, + displayName: action.getDisplayName({ embeddable: api, data, trigger: cellValueTrigger }), + execute: (cellData) => + action.execute({ embeddable: api, data: cellData, trigger: cellValueTrigger }), + })); + }; +} + +export async function getExpressionRendererParams( + state: LensRuntimeState, + { + settings: { syncColors = true, syncCursor = true, syncTooltips = false }, + services, + disableTriggers = false, + getExecutionContext, + searchSessionId, + abortController, + onRender, + handleEvent, + onData = noop, + logError, + api, + addUserMessages, + updateBlockingErrors, + searchContext, + renderCount, + }: GetExpressionRendererPropsParams +): Promise<{ + params: ExpressionWrapperProps | null; + abortController?: AbortController; + indexPatterns: IndexPatternMap; + indexPatternRefs: IndexPatternRef[]; + activeVisualizationState?: unknown; + activeDatasourceState?: unknown; +}> { + const { expressionRenderer, documentToExpression } = services; + + const { + expression, + indexPatterns, + indexPatternRefs, + activeVisualizationState, + activeDatasourceState, + } = await getExpressionFromDocument(state.attributes, documentToExpression); + + // Apparently this change produces had lots of issues with solutions not using + // the Embeddable incorrectly. Will comment for now and later on will restore it when + // https://github.com/elastic/kibana/issues/200236 is resolved + // + // if at least one indexPattern is time based, then the Lens embeddable requires the timeRange prop + // this is necessary for the dataview embeddable but not the ES|QL one + // if ( + // isSearchContextIncompatibleWithDataViews( + // api, + // getExecutionContext(), + // searchContext, + // indexPatternRefs, + // indexPatterns + // ) + // ) { + // addUserMessages([getSearchContextIncompatibleMessage()]); + // } + + if (expression) { + const params: ExpressionWrapperProps = { + expression, + syncColors, + syncCursor, + syncTooltips, + searchSessionId, + onRender$: onRender, + handleEvent, + onData$: onData, + // Remove ES|QL query from it + searchContext: getExecutionSearchContext(searchContext), + interactive: !disableTriggers, + executionContext: getExecutionContext(), + lensInspector: { + getInspectorAdapters: api.getInspectorAdapters, + inspect: api.inspect, + closeInspector: api.closeInspector, + }, + ExpressionRenderer: expressionRenderer, + addUserMessages, + onRuntimeError: (error: Error) => { + updateBlockingErrors(error); + logError('runtime'); + }, + abortController, + hasCompatibleActions: buildHasCompatibleActions(api, services), + getCompatibleCellValueActions: buildGetCompatibleCellValueActions(api, services), + variables: getVariables(api, state), + style: state.style, + className: state.className, + noPadding: state.noPadding, + }; + return { + indexPatterns, + indexPatternRefs, + activeVisualizationState, + activeDatasourceState, + params, + abortController, + }; + } + + return { + params: null, + abortController, + indexPatterns, + indexPatternRefs, + activeVisualizationState, + activeDatasourceState, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/merged_search_context.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/merged_search_context.ts new file mode 100644 index 0000000000000..5b467dd706a69 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/merged_search_context.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public'; +import { + type AggregateQuery, + type Filter, + isOfAggregateQueryType, + type Query, + type TimeRange, + ExecutionContextSearch, +} from '@kbn/es-query'; +import { PublishingSubject, apiPublishesTimeslice } from '@kbn/presentation-publishing'; +import type { LensRuntimeState } from '../types'; +import { nonNullable } from '../../utils'; + +export interface MergedSearchContext { + now: number; + timeRange: TimeRange | undefined; + query: Array<Query | AggregateQuery>; + filters: Filter[]; + disableWarningToasts: boolean; +} + +export function getMergedSearchContext( + { attributes }: LensRuntimeState, + { + filters, + query, + timeRange, + }: { + filters?: Filter[]; + query?: Query | AggregateQuery; + timeRange?: TimeRange; + }, + customTimeRange$: PublishingSubject<TimeRange | undefined>, + parentApi: unknown, + { + data, + injectFilterReferences, + }: { data: DataPublicPluginStart; injectFilterReferences: FilterManager['inject'] } +): MergedSearchContext { + const parentTimeSlice = apiPublishesTimeslice(parentApi) + ? parentApi.timeslice$.getValue() + : undefined; + + const timesliceTimeRange = parentTimeSlice + ? { + from: new Date(parentTimeSlice[0]).toISOString(), + to: new Date(parentTimeSlice[1]).toISOString(), + mode: 'absolute' as 'absolute', + } + : undefined; + + const customTimeRange = customTimeRange$.getValue(); + + const timeRangeToRender = customTimeRange ?? timesliceTimeRange ?? timeRange; + const context = { + now: data.nowProvider.get().getTime(), + timeRange: timeRangeToRender, + query: [attributes.state.query].filter(nonNullable), + filters: injectFilterReferences(attributes.state.filters || [], attributes.references), + disableWarningToasts: true, + }; + // Prepend query and filters from dashboard to the visualization ones + if (query) { + if (!isOfAggregateQueryType(query)) { + context.query.unshift(query); + } + } + if (filters) { + context.filters.unshift(...filters.filter(({ meta }) => !meta.disabled)); + } + return context; +} + +export function getExecutionSearchContext( + searchContext: MergedSearchContext +): ExecutionContextSearch { + if (!isOfAggregateQueryType(searchContext.query[0])) { + return searchContext as ExecutionContextSearch; + } + return { + ...searchContext, + query: [], + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.test.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.test.ts new file mode 100644 index 0000000000000..dfddfe84b57cc --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionRendererEvent } from '@kbn/expressions-plugin/public'; +import { getLensApiMock, getLensRuntimeStateMock, makeEmbeddableServices } from '../mocks'; +import { LensApi, LensEmbeddableStartServices, LensPublicCallbacks } from '../types'; +import { prepareEventHandler } from './on_event'; +import faker from 'faker'; +import { + LENS_EDIT_PAGESIZE_ACTION, + LENS_EDIT_RESIZE_ACTION, + LENS_EDIT_SORT_ACTION, + LENS_TOGGLE_ACTION, +} from '../../visualizations/datatable/components/constants'; + +describe('Embeddable interaction event handlers', () => { + beforeEach(() => { + // LensAPI mock is a static mock, so we need to reset it between tests + jest.resetAllMocks(); + }); + + function getCallbacks(shouldPreventDefault?: boolean) { + if (!shouldPreventDefault) { + return { onFilter: jest.fn(), onBrushEnd: jest.fn(), onTableRowClick: jest.fn() }; + } + return { + onFilter: jest.fn((event) => event.preventDefault()), + onBrushEnd: jest.fn((event) => event.preventDefault()), + onTableRowClick: jest.fn((event) => event.preventDefault()), + }; + } + + function getHandler( + api: LensApi = getLensApiMock(), + callbacks: LensPublicCallbacks = getCallbacks(), + services: LensEmbeddableStartServices = makeEmbeddableServices(), + disableTriggers: boolean = false + ) { + return prepareEventHandler( + api, + jest.fn(() => getLensRuntimeStateMock()), + callbacks, + services, + disableTriggers + ); + } + + function getTable() { + return { columns: { test: { meta: { field: '@timestamp', sourceParams: {} } } } }; + } + + async function submitEvent(event: ExpressionRendererEvent, callPreventDefault: boolean = false) { + const onEditAction = jest.fn(); + const callbacks = getCallbacks(callPreventDefault); + const services = makeEmbeddableServices(undefined, undefined, { + visOverrides: { id: 'lnsXY', onEditAction }, + }); + const lensApi = getLensApiMock(); + const handler = getHandler(lensApi, callbacks, services); + + await handler(event); + + return { + reSubmit: (newEvent: ExpressionRendererEvent) => handler(newEvent), + callbacks, + getTrigger: services.uiActions.getTrigger, + updateAttributes: lensApi.updateAttributes, + onEditAction, + }; + } + + it('should call onTableRowClick event ', async () => { + const event = { + name: 'tableRowContextMenuClick', + data: { rowIndex: 1, table: getTable() }, + }; + const { callbacks } = await submitEvent(event); + expect(callbacks.onTableRowClick).toHaveBeenCalledWith(expect.objectContaining(event.data)); + }); + it('should prevent onTableRowClick trigger when calling preventDefault ', async () => { + const event = { + name: 'tableRowContextMenuClick', + data: { rowIndex: 1, table: getTable() }, + }; + const { getTrigger } = await submitEvent(event, true); + expect(getTrigger).not.toHaveBeenCalled(); + }); + it('should call onBrush event on filter call ', async () => { + const event = { + name: 'brush', + data: { column: 'test', range: [1, 2], table: getTable() }, + }; + const { callbacks } = await submitEvent(event); + expect(callbacks.onBrushEnd).toHaveBeenCalledWith(expect.objectContaining(event.data)); + }); + it('should prevent the onBrush trigger when calling preventDefault', async () => { + const event = { + name: 'brush', + data: { column: 'test', range: [1, 2], table: getTable() }, + }; + const { getTrigger } = await submitEvent(event, true); + expect(getTrigger).not.toHaveBeenCalled(); + }); + it('should call onFilter event on filter call ', async () => { + const event = { + name: 'filter', + data: { + data: [{ value: faker.random.number(), row: 1, column: 'test', table: getTable() }], + }, + }; + const { callbacks } = await submitEvent(event); + expect(callbacks.onFilter).toHaveBeenCalledWith(expect.objectContaining(event.data)); + }); + it('should prevent the onFilter trigger when calling preventDefault', async () => { + const event = { + name: 'filter', + data: { + data: [{ value: faker.random.number(), row: 1, column: 'test', table: getTable() }], + }, + }; + const { getTrigger } = await submitEvent(event, true); + expect(getTrigger).not.toHaveBeenCalled(); + }); + + it('should reload on edit events', async () => { + const { reSubmit, onEditAction, updateAttributes } = await submitEvent({ + name: 'edit', + data: { action: LENS_EDIT_SORT_ACTION }, + }); + + expect(onEditAction).toHaveBeenCalled(); + expect(updateAttributes).toHaveBeenCalled(); + + await reSubmit({ name: 'edit', data: { action: LENS_EDIT_RESIZE_ACTION } }); + + expect(onEditAction).toHaveBeenCalled(); + expect(updateAttributes).toHaveBeenCalled(); + + await reSubmit({ name: 'edit', data: { action: LENS_TOGGLE_ACTION } }); + + expect(onEditAction).toHaveBeenCalled(); + expect(updateAttributes).toHaveBeenCalled(); + + await reSubmit({ name: 'edit', data: { action: LENS_EDIT_PAGESIZE_ACTION } }); + + expect(onEditAction).toHaveBeenCalled(); + expect(updateAttributes).toHaveBeenCalled(); + }); + + it('should not reload on non-edit events', async () => { + const { reSubmit, onEditAction, updateAttributes } = await submitEvent({ + name: 'tableRowContextMenuClick', + data: { rowIndex: 1, table: getTable() }, + }); + + expect(onEditAction).not.toHaveBeenCalled(); + expect(updateAttributes).not.toHaveBeenCalled(); + + await reSubmit({ + name: 'brush', + data: { column: 'test', range: [1, 2], table: getTable() }, + }); + + expect(onEditAction).not.toHaveBeenCalled(); + expect(updateAttributes).not.toHaveBeenCalled(); + + await reSubmit({ + name: 'filter', + data: { + data: [{ value: faker.random.number(), row: 1, column: 'test', table: getTable() }], + }, + }); + + expect(onEditAction).not.toHaveBeenCalled(); + expect(updateAttributes).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.ts new file mode 100644 index 0000000000000..71ce4e15693c8 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/on_event.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 { ExpressionRendererEvent } from '@kbn/expressions-plugin/public'; +import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; +import { type AggregateQuery, type Query, isOfAggregateQueryType } from '@kbn/es-query'; +import { + isLensBrushEvent, + isLensEditEvent, + isLensFilterEvent, + isLensMultiFilterEvent, + isLensTableRowContextMenuClickEvent, +} from '../../types'; +import { inferTimeField } from '../../utils'; +import type { + GetStateType, + LensApi, + LensEmbeddableStartServices, + LensPublicCallbacks, +} from '../types'; +import { isTextBasedLanguage } from '../helper'; +import { addLog } from '../logger'; + +export const prepareEventHandler = + ( + api: LensApi, + getState: GetStateType, + callbacks: LensPublicCallbacks, + { data, uiActions, visualizationMap }: LensEmbeddableStartServices, + disableTriggers: boolean | undefined + ) => + async (event: ExpressionRendererEvent) => { + if (!uiActions?.getTrigger || disableTriggers) { + return; + } + addLog(`onEvent$`); + + let eventHandler: + | LensPublicCallbacks['onBrushEnd'] + | LensPublicCallbacks['onFilter'] + | LensPublicCallbacks['onTableRowClick']; + let shouldExecuteDefaultTriggers = true; + + if (isLensBrushEvent(event)) { + eventHandler = callbacks.onBrushEnd; + } else if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) { + eventHandler = callbacks.onFilter; + } else if (isLensTableRowContextMenuClickEvent(event)) { + eventHandler = callbacks.onTableRowClick; + } + const currentState = getState(); + + eventHandler?.({ + ...event.data, + preventDefault: () => { + shouldExecuteDefaultTriggers = false; + }, + }); + + if (isLensFilterEvent(event) || isLensMultiFilterEvent(event) || isLensBrushEvent(event)) { + if (shouldExecuteDefaultTriggers) { + // if the embeddable is located in an app where there is the Unified search bar with the ES|QL editor, then use this query + // otherwise use the query from the saved object + let esqlQuery: AggregateQuery | Query | undefined; + if (isTextBasedLanguage(currentState)) { + const query = data.query.queryString.getQuery(); + esqlQuery = isOfAggregateQueryType(query) ? query : currentState.attributes.state.query; + } + uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: { + ...event.data, + timeFieldName: + event.data.timeFieldName || inferTimeField(data.datatableUtilities, event), + query: esqlQuery, + }, + embeddable: api, + }); + } + } + + if (isLensTableRowContextMenuClickEvent(event)) { + if (shouldExecuteDefaultTriggers) { + uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( + { + data: event.data, + embeddable: api, + }, + true + ); + } + } + + const onEditAction = currentState.attributes.visualizationType + ? visualizationMap[currentState.attributes.visualizationType]?.onEditAction + : undefined; + + // We allow for edit actions in the Embeddable for display purposes only (e.g. changing the datatable sort order). + // No state changes made here with an edit action are persisted. + if (isLensEditEvent(event) && onEditAction) { + // updating the state would trigger a reload + api.updateAttributes({ + ...currentState.attributes, + state: { + ...currentState.attributes.state, + visualization: onEditAction(currentState.attributes.state.visualization, event), + }, + }); + } + }; diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/on_render.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/on_render.ts new file mode 100644 index 0000000000000..ba0a47b5944e3 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/on_render.ts @@ -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 { KibanaExecutionContext } from '@kbn/core-execution-context-common'; +import { canTrackContentfulRender } from '@kbn/presentation-containers'; +import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; +import { TableInspectorAdapter } from '../../editor_frame_service/types'; + +import { getExecutionContextEvents, trackUiCounterEvents } from '../../lens_ui_telemetry'; +import { GetStateType, LensApi, LensEmbeddableStartServices, LensInternalApi } from '../types'; +import { getSuccessfulRequestTimings } from '../../report_performance_metric_util'; +import { addLog } from '../logger'; + +function trackContentfulRender(activeData: TableInspectorAdapter, parentApi: unknown) { + if (!canTrackContentfulRender(parentApi)) { + return; + } + + const hasData = Object.values(activeData).some((table) => { + if (table.meta?.statistics?.totalCount != null) { + // if totalCount is set, refer to total count + return table.meta.statistics.totalCount > 0; + } + // if not, fall back to check the rows of the table + return table.rows.length > 0; + }); + + if (hasData) { + parentApi.trackContentfulRender(); + } +} + +function trackPerformanceMetrics( + api: LensApi, + coreStart: LensEmbeddableStartServices['coreStart'] +) { + const inspectorAdapters = api.getInspectorAdapters(); + const timings = getSuccessfulRequestTimings(inspectorAdapters); + if (timings) { + const esRequestMetrics = { + eventName: 'lens_chart_es_request_totals', + duration: timings.requestTimeTotal, + key1: 'es_took_total', + value1: timings.esTookTotal, + }; + reportPerformanceMetricEvent(coreStart.analytics, esRequestMetrics); + } +} + +export function prepareOnRender( + api: LensApi, + internalApi: LensInternalApi, + parentApi: unknown, + getState: GetStateType, + { datasourceMap, visualizationMap, coreStart }: LensEmbeddableStartServices, + executionContext: KibanaExecutionContext | undefined, + dispatchRenderComplete: () => void +) { + return function onRender$(count: number) { + addLog(`onRender$ ${count}`); + // for some reason onRender$ is emitting multiple times with the same render count + // so avoid to repeat the same logic on duplicate calls + if (count === internalApi.renderCount$.getValue()) { + return; + } + let datasourceEvents: string[] = []; + let visualizationEvents: string[] = []; + const currentState = getState(); + + if (currentState) { + datasourceEvents = Object.values(datasourceMap).reduce<string[]>( + (acc, datasource) => [ + ...acc, + ...(datasource.getRenderEventCounters?.( + currentState.attributes.state.datasourceStates[datasource.id] + ) ?? []), + ], + [] + ); + + if (currentState.attributes.visualizationType) { + visualizationEvents = + visualizationMap[currentState.attributes.visualizationType].getRenderEventCounters?.( + currentState.attributes.state.visualization + ) ?? []; + } + } + + const events = [ + ...datasourceEvents, + ...visualizationEvents, + ...getExecutionContextEvents(executionContext), + ]; + + const adHocDataViews = Object.values(currentState.attributes.state.adHocDataViews || {}); + adHocDataViews.forEach(() => { + events.push('ad_hoc_data_view'); + }); + + trackUiCounterEvents(events, executionContext); + + trackContentfulRender(api.getInspectorAdapters().tables?.tables, parentApi); + + dispatchRenderComplete(); + + trackPerformanceMetrics(api, coreStart); + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/telemetry.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/telemetry.ts new file mode 100644 index 0000000000000..ede2f1b0aaf37 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/telemetry.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 type { KibanaExecutionContext } from '@kbn/core/public'; +import { trackUiCounterEvents } from '../../lens_ui_telemetry'; + +export function getLogError(getExecutionContext: () => KibanaExecutionContext | undefined) { + return (type: 'runtime' | 'validation') => { + trackUiCounterEvents( + type === 'runtime' ? 'embeddable_runtime_error' : 'embeddable_validation_error', + getExecutionContext() + ); + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/update_data_views.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/update_data_views.ts new file mode 100644 index 0000000000000..0e7f130d339db --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/update_data_views.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uniqBy } from 'lodash'; +import { getIndexPatternsObjects } from '../../utils'; +import { LensEmbeddableStartServices, LensRuntimeState } from '../types'; + +export async function getUsedDataViews( + references: LensRuntimeState['attributes']['references'], + adHocDataViewsSpecs: LensRuntimeState['attributes']['state']['adHocDataViews'], + dataViews: LensEmbeddableStartServices['dataViews'] +) { + const [{ indexPatterns }, ...adHocDataViews] = await Promise.all([ + getIndexPatternsObjects(references.map(({ id }) => id) || [], dataViews), + ...Object.values(adHocDataViewsSpecs || {}).map((spec) => dataViews.create(spec)), + ]); + + return uniqBy(indexPatterns.concat(adHocDataViews), 'id'); +} diff --git a/x-pack/plugins/lens/public/react_embeddable/expressions/variables.ts b/x-pack/plugins/lens/public/react_embeddable/expressions/variables.ts new file mode 100644 index 0000000000000..c1fdda750199f --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/expressions/variables.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 type { Datatable } from '@kbn/expressions-plugin/common'; +import type { TextBasedPersistedState } from '../../datasources/text_based/types'; +import { LensApi, LensRuntimeState } from '../types'; + +function getInternalTables(states: Record<string, unknown>) { + const result: Record<string, Datatable> = {}; + if ('textBased' in states) { + const layers = (states.textBased as TextBasedPersistedState).layers; + for (const layer in layers) { + if (layers[layer]?.table) { + result[layer] = layers[layer].table!; + } + } + } + return result; +} + +/** + * Collect all the data that need to be forwarded at the end of the + * expression pipeline as overrides, palette, etc... and merged them all here + */ +export function getVariables(api: LensApi, state: LensRuntimeState) { + return { + embeddableTitle: api.defaultPanelTitle?.getValue(), + ...(state.palette ? { theme: { palette: state.palette } } : {}), + ...('overrides' in state ? { overrides: state.overrides } : {}), + ...getInternalTables(state.attributes.state.datasourceStates), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/helper.test.ts b/x-pack/plugins/lens/public/react_embeddable/helper.test.ts new file mode 100644 index 0000000000000..33a8d0d0093d4 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/helper.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultDoc, makeAttributeService } from '../mocks/services_mock'; +import { deserializeState } from './helper'; + +describe('Embeddable helpers', () => { + describe('deserializeState', () => { + it('should forward a by value raw state', async () => { + const attributeService = makeAttributeService(defaultDoc); + const rawState = { + attributes: defaultDoc, + }; + const runtimeState = await deserializeState(attributeService, rawState); + expect(runtimeState).toEqual(rawState); + }); + + it('should wrap Lens doc/attributes into component state shape', async () => { + const attributeService = makeAttributeService(defaultDoc); + const runtimeState = await deserializeState(attributeService, defaultDoc); + expect(runtimeState).toEqual( + expect.objectContaining({ + attributes: { ...defaultDoc, references: defaultDoc.references }, + }) + ); + }); + + it('load a by-ref doc from the attribute service', async () => { + const attributeService = makeAttributeService(defaultDoc); + await deserializeState(attributeService, { + savedObjectId: '123', + }); + + expect(attributeService.loadFromLibrary).toHaveBeenCalledWith('123'); + }); + + it('should fallback to an empty Lens doc if the saved object is not found', async () => { + const attributeService = makeAttributeService(defaultDoc); + attributeService.loadFromLibrary.mockRejectedValueOnce(new Error('not found')); + const runtimeState = await deserializeState(attributeService, { + savedObjectId: '123', + }); + // check the visualizationType set to null for empty state + expect(runtimeState.attributes.visualizationType).toBeNull(); + }); + + describe('injected references should overwrite inner ones', () => { + // There are 3 possible scenarios here for reference injections: + // * default space for a by-value + // * default space for a by-ref with a "lens" panel reference type + // * other space for a by-value with new ref ids + + it('should inject correctly serialized references into runtime state for a by value in the default space', async () => { + const attributeService = makeAttributeService(defaultDoc); + const mockedReferences = [ + { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, + ]; + const runtimeState = await deserializeState( + attributeService, + { + attributes: defaultDoc, + }, + mockedReferences + ); + expect(attributeService.injectReferences).toHaveBeenCalled(); + expect(runtimeState.attributes.references).toEqual(mockedReferences); + }); + + it('should inject correctly serialized references into runtime state for a by ref in the default space', async () => { + const attributeService = makeAttributeService(defaultDoc); + const mockedReferences = [ + { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, + ]; + const runtimeState = await deserializeState( + attributeService, + { + savedObjectId: '123', + }, + mockedReferences + ); + expect(attributeService.injectReferences).not.toHaveBeenCalled(); + // Note the original references should be kept + expect(runtimeState.attributes.references).toEqual(defaultDoc.references); + }); + + it('should inject correctly serialized references into runtime state for a by value in another space', async () => { + const attributeService = makeAttributeService(defaultDoc); + const mockedReferences = [ + { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, + ]; + const runtimeState = await deserializeState( + attributeService, + { + attributes: defaultDoc, + }, + mockedReferences + ); + expect(attributeService.injectReferences).toHaveBeenCalled(); + // note: in this case the references are swapped + expect(runtimeState.attributes.references).toEqual(mockedReferences); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/react_embeddable/helper.ts b/x-pack/plugins/lens/public/react_embeddable/helper.ts new file mode 100644 index 0000000000000..3ee63d907068d --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/helper.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + apiHasParentApi, + apiPublishesViewMode, + getInheritedViewMode, + ViewMode, + type PublishingSubject, + apiHasExecutionContext, +} from '@kbn/presentation-publishing'; +import { isObject } from 'lodash'; +import { BehaviorSubject } from 'rxjs'; +import fastIsEqual from 'fast-deep-equal'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { RenderMode } from '@kbn/expressions-plugin/common'; +import { SavedObjectReference } from '@kbn/core/types'; +import { LensRuntimeState, LensSerializedState } from './types'; +import type { LensAttributesService } from '../lens_attribute_service'; + +export function createEmptyLensState( + visualizationType: null | string = null, + title?: LensSerializedState['title'], + description?: LensSerializedState['description'], + query?: LensSerializedState['query'], + filters?: LensSerializedState['filters'] +) { + const isTextBased = query && isOfAggregateQueryType(query); + return { + attributes: { + title: title ?? '', + description: description ?? '', + visualizationType, + references: [], + state: { + query: query || { query: '', language: 'kuery' }, + filters: filters || [], + internalReferences: [], + datasourceStates: { ...(isTextBased ? { text_based: {} } : { form_based: {} }) }, + visualization: {}, + }, + }, + }; +} + +// Shared logic to ensure the attributes are correctly loaded +// Make sure to inject references from the container down to the runtime state +// this ensure migrations/copy to spaces works correctly +export async function deserializeState( + attributeService: LensAttributesService, + rawState: LensSerializedState, + references?: SavedObjectReference[] +) { + if (rawState.savedObjectId) { + try { + const { attributes, managed, sharingSavedObjectProps } = + await attributeService.loadFromLibrary(rawState.savedObjectId); + return { ...rawState, attributes, managed, sharingSavedObjectProps }; + } catch (e) { + // return an empty Lens document if no saved object is found + return { ...rawState, attributes: createEmptyLensState().attributes }; + } + } + // Inject applied only to by-value SOs + return attributeService.injectReferences( + ('attributes' in rawState ? rawState : { attributes: rawState }) as LensRuntimeState, + references?.length ? references : undefined + ); +} + +export function emptySerializer() { + return {}; +} + +export type ComparatorType<T extends unknown> = [ + BehaviorSubject<T>, + (newValue: T) => void, + (a: T, b: T) => boolean +]; + +export function makeComparator<T extends unknown>( + observable: BehaviorSubject<T> +): ComparatorType<T> { + return [observable, (newValue: T) => observable.next(newValue), fastIsEqual]; +} + +/** + * Helper function to either extract an observable from an API or create a new one + * with a default value to start with. + * Note that extracting from the API will make subscription emit if the value changes upstream + * as it keeps the original reference without cloning. + * @returns the observable and a comparator to use for detecting "unsaved changes" on it + */ +export function buildObservableVariable<T extends unknown>( + variable: T | PublishingSubject<T> +): [BehaviorSubject<T>, ComparatorType<T>] { + if (variable instanceof BehaviorSubject) { + return [variable, makeComparator(variable)]; + } + const variable$ = new BehaviorSubject<T>(variable as T); + return [variable$, makeComparator(variable$)]; +} + +export function isTextBasedLanguage(state: LensRuntimeState) { + return isOfAggregateQueryType(state.attributes?.state.query); +} + +export function getViewMode(api: unknown) { + return apiPublishesViewMode(api) ? getInheritedViewMode(api) : undefined; +} + +export function getRenderMode(api: unknown): RenderMode { + const mode = getViewMode(api) ?? 'view'; + return mode === 'print' ? 'view' : mode; +} + +function apiHasExecutionContextFunction( + api: unknown +): api is { getAppContext: () => { currentAppId: string } } { + return isObject(api) && 'getAppContext' in api && typeof api.getAppContext === 'function'; +} + +export function getParentContext(parentApi: unknown) { + if (apiHasExecutionContext(parentApi)) { + return parentApi.executionContext; + } + if (apiHasExecutionContextFunction(parentApi)) { + return { type: parentApi.getAppContext().currentAppId }; + } + return; +} + +export function extractInheritedViewModeObservable( + parentApi?: unknown +): PublishingSubject<ViewMode> { + if (apiPublishesViewMode(parentApi)) { + return parentApi.viewMode; + } + if (apiHasParentApi(parentApi)) { + return extractInheritedViewModeObservable(parentApi.parentApi); + } + return new BehaviorSubject<ViewMode>('view'); +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.test.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.test.ts new file mode 100644 index 0000000000000..a4f84c329bd3c --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import faker from 'faker'; +import type { LensRuntimeState, VisualizationContext } from '../types'; +import { initializeActionApi } from './initialize_actions'; +import { + getLensApiMock, + makeEmbeddableServices, + getLensRuntimeStateMock, + getVisualizationContextHelperMock, + createUnifiedSearchApi, +} from '../mocks'; +import { createEmptyLensState } from '../helper'; +const DATAVIEW_ID = 'myDataView'; + +jest.mock('../../app_plugin/show_underlying_data', () => { + return { + ...jest.requireActual('../../app_plugin/show_underlying_data'), + getLayerMetaInfo: jest.fn(() => ({ + meta: { + id: DATAVIEW_ID, + columns: ['a', 'b'], + filters: { disabled: [], enabled: [] }, + }, + error: undefined, + isVisible: true, + })), + }; +}); + +function setupActionsApi( + stateOverrides?: Partial<LensRuntimeState>, + contextOverrides?: Omit<VisualizationContext, 'doc'> +) { + const services = makeEmbeddableServices(undefined, undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'form_based' }, + }); + const uuid = faker.random.uuid(); + const runtimeState = getLensRuntimeStateMock(stateOverrides); + const apiMock = getLensApiMock(); + + const { api } = initializeActionApi( + uuid, + runtimeState, + () => runtimeState, + createUnifiedSearchApi(), + pick(apiMock, ['timeRange$']), + pick(apiMock, ['panelTitle']), + getVisualizationContextHelperMock(stateOverrides?.attributes, contextOverrides), + { + ...services, + data: { + ...services.data, + nowProvider: { ...services.data.nowProvider, get: jest.fn(() => new Date()) }, + }, + } + ); + return api; +} + +describe('Dashboard actions', () => { + describe('Drilldowns', () => { + it('should expose drilldowns for DSL based visualization', async () => { + const api = setupActionsApi(); + expect(api.enhancements).toBeDefined(); + }); + + it('should not expose drilldowns for ES|QL chart types', async () => { + const api = setupActionsApi( + createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { + esql: 'FROM index', + }) + ); + expect(api.enhancements).toBeUndefined(); + }); + }); + + describe('Explore in Discover', () => { + // make it pass the basic check on viewUnderlyingData + const visualizationContextMockOverrides = { + mergedSearchContext: {}, + indexPatterns: { + [DATAVIEW_ID]: { + id: DATAVIEW_ID, + title: 'idx1', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + ], + getFieldByName: jest.fn(), + isPersisted: true, + spec: {}, + }, + }, + indexPatternRefs: [], + activeVisualizationState: {}, + activeDatasourceState: {}, + activeData: {}, + }; + it('should expose the "explore in discover" capability for DSL based visualization when compatible', async () => { + const api = setupActionsApi(undefined, visualizationContextMockOverrides); + api.loadViewUnderlyingData(); + expect(api.canViewUnderlyingData$.getValue()).toBe(true); + }); + + it('should expose the "explore in discover" capability for ES|QL chart types', async () => { + const api = setupActionsApi( + createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { + esql: 'FROM index', + }), + visualizationContextMockOverrides + ); + api.loadViewUnderlyingData(); + expect(api.canViewUnderlyingData$.getValue()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.ts new file mode 100644 index 0000000000000..65fd13c8fca50 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_actions.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Capabilities } from '@kbn/core-capabilities-common'; +import { getEsQueryConfig } from '@kbn/data-plugin/public'; +import { + AggregateQuery, + EsQueryConfig, + Filter, + Query, + TimeRange, + isOfQueryType, +} from '@kbn/es-query'; +import { + PublishingSubject, + StateComparators, + apiPublishesUnifiedSearch, + getUnchangingComparator, +} from '@kbn/presentation-publishing'; +import { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public'; +import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin'; +import { partition } from 'lodash'; +import { Visualization } from '../..'; +import { combineQueryAndFilters, getLayerMetaInfo } from '../../app_plugin/show_underlying_data'; +import { TableInspectorAdapter } from '../../editor_frame_service/types'; + +import { Datasource, IndexPatternMap } from '../../types'; +import { getMergedSearchContext } from '../expressions/merged_search_context'; +import { buildObservableVariable, isTextBasedLanguage } from '../helper'; +import type { + GetStateType, + LensEmbeddableStartServices, + LensRuntimeState, + ViewInDiscoverCallbacks, + ViewUnderlyingDataArgs, + VisualizationContextHelper, +} from '../types'; +import { getActiveDatasourceIdFromDoc, getActiveVisualizationIdFromDoc } from '../../utils'; + +function getViewUnderlyingDataArgs({ + activeDatasource, + activeDatasourceState, + activeVisualization, + activeVisualizationState, + activeData, + dataViews, + capabilities, + query, + filters, + timeRange, + esQueryConfig, +}: { + activeDatasource: Datasource; + activeDatasourceState: unknown; + activeVisualization: Visualization; + activeVisualizationState: unknown; + activeData: TableInspectorAdapter | undefined; + dataViews: IndexPatternMap; + capabilities: { + canSaveVisualizations: boolean; + canOpenVisualizations: boolean; + canSaveDashboards: boolean; + navLinks: Capabilities['navLinks']; + discover: Capabilities['discover']; + }; + query: Array<Query | AggregateQuery>; + filters: Filter[]; + timeRange: TimeRange; + esQueryConfig: EsQueryConfig; +}) { + const { error, meta } = getLayerMetaInfo( + activeDatasource, + activeDatasourceState, + activeVisualization, + activeVisualizationState, + activeData, + dataViews, + timeRange, + capabilities + ); + + if (error || !meta) { + return; + } + const luceneOrKuery: Query[] = []; + const aggregateQueries: AggregateQuery[] = []; + + if (Array.isArray(query)) { + const [kqlOrLuceneQueries, esqlQueries] = partition(query, isOfQueryType); + if (kqlOrLuceneQueries.length) { + luceneOrKuery.push(...kqlOrLuceneQueries); + } + if (esqlQueries.length) { + aggregateQueries.push(...esqlQueries); + } + } + + const { filters: newFilters, query: newQuery } = combineQueryAndFilters( + luceneOrKuery.length > 0 ? luceneOrKuery : aggregateQueries[0], + filters, + meta, + Object.values(dataViews), + esQueryConfig + ); + + const dataViewSpec = dataViews[meta.id]!.spec; + + return { + dataViewSpec, + timeRange, + filters: newFilters, + query: aggregateQueries.length > 0 ? aggregateQueries[0] : newQuery, + columns: meta.columns, + }; +} + +function loadViewUnderlyingDataArgs( + state: LensRuntimeState, + { getVisualizationContext }: VisualizationContextHelper, + searchContextApi: { timeRange$: PublishingSubject<TimeRange | undefined> }, + parentApi: unknown, + { + capabilities, + uiSettings, + injectFilterReferences, + data, + datasourceMap, + visualizationMap, + }: LensEmbeddableStartServices +) { + const { doc, activeData, activeDatasourceState, activeVisualizationState, indexPatterns } = + getVisualizationContext(); + const activeVisualizationId = getActiveVisualizationIdFromDoc(doc); + const activeDatasourceId = getActiveDatasourceIdFromDoc(doc); + const activeVisualization = activeVisualizationId + ? visualizationMap[activeVisualizationId] + : undefined; + const activeDatasource = activeDatasourceId ? datasourceMap[activeDatasourceId] : undefined; + if ( + !doc || + !activeData || + !activeDatasource || + !activeDatasourceState || + !activeVisualization || + !activeVisualizationState + ) { + return; + } + + const { filters$, query$, timeRange$ } = apiPublishesUnifiedSearch(parentApi) + ? parentApi + : { filters$: undefined, query$: undefined, timeRange$: undefined }; + + const mergedSearchContext = getMergedSearchContext( + state, + { + filters: filters$?.getValue(), + query: query$?.getValue(), + timeRange: timeRange$?.getValue(), + }, + searchContextApi.timeRange$, + parentApi, + { + data, + injectFilterReferences, + } + ); + + if (!mergedSearchContext.timeRange) { + return; + } + + const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({ + activeDatasource, + activeDatasourceState, + activeVisualization, + activeVisualizationState, + activeData, + capabilities: { + canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), + canSaveVisualizations: Boolean(capabilities.visualize.save), + canOpenVisualizations: Boolean(capabilities.visualize.show), + navLinks: capabilities.navLinks, + discover: capabilities.discover, + }, + query: mergedSearchContext.query, + filters: mergedSearchContext.filters || [], + timeRange: mergedSearchContext.timeRange, + esQueryConfig: getEsQueryConfig(uiSettings), + dataViews: indexPatterns, + }); + + return viewUnderlyingDataArgs; +} + +function createViewUnderlyingDataApis( + getState: GetStateType, + visualizationContextHelper: VisualizationContextHelper, + searchContextApi: { timeRange$: PublishingSubject<TimeRange | undefined> }, + parentApi: unknown, + services: LensEmbeddableStartServices +): ViewInDiscoverCallbacks { + let viewUnderlyingDataArgs: undefined | ViewUnderlyingDataArgs; + + const [canViewUnderlyingData$] = buildObservableVariable<boolean>(false); + + return { + canViewUnderlyingData$, + loadViewUnderlyingData: () => { + viewUnderlyingDataArgs = loadViewUnderlyingDataArgs( + getState(), + visualizationContextHelper, + searchContextApi, + parentApi, + services + ); + canViewUnderlyingData$.next(viewUnderlyingDataArgs != null); + }, + getViewUnderlyingDataArgs: () => { + return viewUnderlyingDataArgs; + }, + }; +} + +/** + * Initialize APIs used for actions on Lens panels + * This includes drilldowns, explore data, and more + */ +export function initializeActionApi( + uuid: string, + initialState: LensRuntimeState, + getLatestState: GetStateType, + parentApi: unknown, + searchContextApi: { timeRange$: PublishingSubject<TimeRange | undefined> }, + titleApi: { panelTitle: PublishingSubject<string | undefined> }, + visualizationContextHelper: VisualizationContextHelper, + services: LensEmbeddableStartServices +): { + api: ViewInDiscoverCallbacks & HasDynamicActions; + comparators: StateComparators<DynamicActionsSerializedState>; + serialize: () => {}; + cleanup: () => void; +} { + const dynamicActionsApi = services.embeddableEnhanced?.initializeReactEmbeddableDynamicActions( + uuid, + () => titleApi.panelTitle.getValue(), + initialState + ); + const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions(); + + return { + api: { + ...(isTextBasedLanguage(initialState) ? {} : dynamicActionsApi?.dynamicActionsApi ?? {}), + ...createViewUnderlyingDataApis( + getLatestState, + visualizationContextHelper, + searchContextApi, + parentApi, + services + ), + }, + comparators: { + ...(dynamicActionsApi?.dynamicActionsComparator ?? { + enhancements: getUnchangingComparator(), + }), + }, + serialize: () => dynamicActionsApi?.serializeDynamicActions() ?? {}, + cleanup: () => { + maybeStopDynamicActions?.stopDynamicActions(); + }, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts new file mode 100644 index 0000000000000..2a0c469b3bbfb --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.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 type { LensRuntimeState } from '../types'; +import { getLensRuntimeStateMock, getLensInternalApiMock, makeEmbeddableServices } from '../mocks'; +import { initializeStateManagement } from './initialize_state_management'; +import { initializeDashboardServices } from './initialize_dashboard_services'; +import faker from 'faker'; +import { createEmptyLensState } from '../helper'; + +function setupDashboardServicesApi(runtimeOverrides?: Partial<LensRuntimeState>) { + const services = makeEmbeddableServices(); + const internalApiMock = getLensInternalApiMock(); + const runtimeState = getLensRuntimeStateMock(runtimeOverrides); + const stateManagementConfig = initializeStateManagement(runtimeState, internalApiMock); + const { api } = initializeDashboardServices( + runtimeState, + () => runtimeState, + internalApiMock, + stateManagementConfig, + {}, + services + ); + return api; +} + +describe('Transformation API', () => { + it("should not save to library if there's already a saveObjectId", async () => { + const api = setupDashboardServicesApi({ savedObjectId: faker.random.uuid() }); + expect(await api.canLinkToLibrary()).toBe(false); + }); + + it("should save to library if there's no saveObjectId declared", async () => { + const api = setupDashboardServicesApi(); + expect(await api.canLinkToLibrary()).toBe(true); + }); + + it('should not save to library for ES|QL chart types', async () => { + // setup a state with an ES|QL query + const api = setupDashboardServicesApi( + createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { + esql: 'FROM index', + }) + ); + expect(await api.canLinkToLibrary()).toBe(false); + }); +}); diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts new file mode 100644 index 0000000000000..d030a92a02b59 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noop } from 'lodash'; +import { + HasInPlaceLibraryTransforms, + HasLibraryTransforms, + PublishesWritablePanelTitle, + PublishesWritablePanelDescription, + SerializedTitles, + StateComparators, + getUnchangingComparator, + initializeTitles, +} from '@kbn/presentation-publishing'; +import { apiPublishesSettings } from '@kbn/presentation-containers'; +import { buildObservableVariable, isTextBasedLanguage } from '../helper'; +import type { + LensComponentProps, + LensPanelProps, + LensRuntimeState, + LensEmbeddableStartServices, + LensOverrides, + LensSharedProps, + IntegrationCallbacks, + LensInternalApi, +} from '../types'; +import { apiHasLensComponentProps } from '../type_guards'; +import { StateManagementConfig } from './initialize_state_management'; + +// Convenience type for the serialized props of this initializer +type SerializedProps = SerializedTitles & LensPanelProps & LensOverrides & LensSharedProps; + +export interface DashboardServicesConfig { + api: PublishesWritablePanelTitle & + PublishesWritablePanelDescription & + HasInPlaceLibraryTransforms & + HasLibraryTransforms<LensRuntimeState> & + Pick<IntegrationCallbacks, 'updateOverrides' | 'getTriggerCompatibleActions'>; + serialize: () => SerializedProps; + comparators: StateComparators<SerializedProps & { isNewPanel?: boolean }>; + cleanup: () => void; +} + +/** + * Everything about panel and library services + */ +export function initializeDashboardServices( + initialState: LensRuntimeState, + getLatestState: () => LensRuntimeState, + internalApi: LensInternalApi, + stateConfig: StateManagementConfig, + parentApi: unknown, + { attributeService, uiActions }: LensEmbeddableStartServices +): DashboardServicesConfig { + const { titlesApi, serializeTitles, titleComparators } = initializeTitles(initialState); + // For some legacy reason the title and description default value is picked differently + // ( based on existing FTR tests ). + const [defaultPanelTitle$] = buildObservableVariable<string | undefined>( + initialState.title || internalApi.attributes$.getValue().title + ); + const [defaultPanelDescription$] = buildObservableVariable<string | undefined>( + initialState.savedObjectId + ? internalApi.attributes$.getValue().description || initialState.description + : initialState.description + ); + // The observable references here are the same to the internalApi, + // the buildObservableVariable re-uses the same observable when detected but it builds the right comparator + const [overrides$, overridesComparator] = buildObservableVariable<LensOverrides['overrides']>( + internalApi.overrides$ + ); + const [disableTriggers$, disabledTriggersComparator] = buildObservableVariable< + boolean | undefined + >(internalApi.disableTriggers$); + + return { + api: { + defaultPanelTitle: defaultPanelTitle$, + defaultPanelDescription: defaultPanelDescription$, + ...titlesApi, + libraryId$: stateConfig.api.savedObjectId, + updateOverrides: internalApi.updateOverrides, + getTriggerCompatibleActions: uiActions.getTriggerCompatibleActions, + // The functions below brings the HasInPlaceLibraryTransforms compliance (new interface) + saveToLibrary: async (title: string) => { + const { attributes } = getLatestState(); + const savedObjectId = await attributeService.saveToLibrary( + { + ...attributes, + title, + }, + attributes.references + ); + // keep in sync the state + stateConfig.api.updateSavedObjectId(savedObjectId); + return savedObjectId; + }, + checkForDuplicateTitle: async ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => { + await attributeService.checkForDuplicateTitle({ + newTitle, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newCopyOnSave: false, + newDescription: '', + displayName: '', + lastSavedTitle: '', + copyOnSave: false, + }); + }, + canLinkToLibrary: async () => + !getLatestState().savedObjectId && !isTextBasedLanguage(getLatestState()), + canUnlinkFromLibrary: async () => Boolean(getLatestState().savedObjectId), + unlinkFromLibrary: () => { + // broadcast the change to the main state serializer + stateConfig.api.updateSavedObjectId(undefined); + + if ((titlesApi.panelTitle.getValue() ?? '').length === 0) { + titlesApi.setPanelTitle(defaultPanelTitle$.getValue()); + } + if ((titlesApi.panelDescription.getValue() ?? '').length === 0) { + titlesApi.setPanelDescription(defaultPanelDescription$.getValue()); + } + defaultPanelTitle$.next(undefined); + defaultPanelDescription$.next(undefined); + }, + getByValueRuntimeSnapshot: (): Omit<LensRuntimeState, 'savedObjectId'> => { + const { savedObjectId, ...rest } = getLatestState(); + return rest; + }, + // The functions below brings the HasLibraryTransforms compliance (old interface) + getByReferenceState: () => getLatestState(), + getByValueState: (): Omit<LensRuntimeState, 'savedObjectId'> => { + const { savedObjectId, ...rest } = getLatestState(); + return rest; + }, + }, + serialize: () => { + const { style, noPadding, className } = apiHasLensComponentProps(parentApi) + ? parentApi + : ({} as LensComponentProps); + const settings = apiPublishesSettings(parentApi) + ? { + syncColors: parentApi.settings.syncColors$.getValue(), + syncCursor: parentApi.settings.syncCursor$.getValue(), + syncTooltips: parentApi.settings.syncTooltips$.getValue(), + } + : {}; + return { + ...serializeTitles(), + style, + noPadding, + className, + ...settings, + palette: initialState.palette, + overrides: overrides$.getValue(), + disableTriggers: disableTriggers$.getValue(), + }; + }, + comparators: { + ...titleComparators, + id: getUnchangingComparator<SerializedTitles & LensPanelProps, 'id'>(), + palette: getUnchangingComparator<SerializedTitles & LensPanelProps, 'palette'>(), + renderMode: getUnchangingComparator<SerializedTitles & LensPanelProps, 'renderMode'>(), + syncColors: getUnchangingComparator<SerializedTitles & LensPanelProps, 'syncColors'>(), + syncCursor: getUnchangingComparator<SerializedTitles & LensPanelProps, 'syncCursor'>(), + syncTooltips: getUnchangingComparator<SerializedTitles & LensPanelProps, 'syncTooltips'>(), + executionContext: getUnchangingComparator<LensSharedProps, 'executionContext'>(), + noPadding: getUnchangingComparator<LensSharedProps, 'noPadding'>(), + viewMode: getUnchangingComparator<LensSharedProps, 'viewMode'>(), + style: getUnchangingComparator<LensSharedProps, 'style'>(), + className: getUnchangingComparator<LensSharedProps, 'className'>(), + overrides: overridesComparator, + disableTriggers: disabledTriggersComparator, + isNewPanel: getUnchangingComparator<{ isNewPanel?: boolean }, 'isNewPanel'>(), + }, + cleanup: noop, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_edit.tsx b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_edit.tsx new file mode 100644 index 0000000000000..81372dad339f7 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_edit.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + HasEditCapabilities, + HasSupportedTriggers, + PublishesDisabledActionIds, + PublishesViewMode, + ViewMode, + apiHasAppContext, + apiPublishesDisabledActionIds, +} from '@kbn/presentation-publishing'; +import { ENABLE_ESQL } from '@kbn/esql-utils'; +import { noop } from 'lodash'; +import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; +import { tracksOverlays } from '@kbn/presentation-containers'; +import { i18n } from '@kbn/i18n'; +import { APP_ID, getEditPath } from '../../../common/constants'; +import { + GetStateType, + LensEmbeddableStartServices, + LensInspectorAdapters, + LensInternalApi, + LensRuntimeState, +} from '../types'; +import { + buildObservableVariable, + emptySerializer, + extractInheritedViewModeObservable, +} from '../helper'; +import { prepareInlineEditPanel } from '../inline_editing/setup_inline_editing'; +import { setupPanelManagement } from '../inline_editing/panel_management'; +import { mountInlineEditPanel } from '../inline_editing/mount'; +import { StateManagementConfig } from './initialize_state_management'; +import { apiPublishesInlineEditingCapabilities } from '../type_guards'; + +function getSupportedTriggers( + getState: GetStateType, + visualizationMap: LensEmbeddableStartServices['visualizationMap'] +) { + return () => { + const currentState = getState(); + if (currentState.attributes?.visualizationType) { + return visualizationMap[currentState.attributes.visualizationType]?.triggers || []; + } + return []; + }; +} + +/** + * Initialize the edit API for the embeddable + **/ +export function initializeEditApi( + uuid: string, + initialState: LensRuntimeState, + getState: GetStateType, + internalApi: LensInternalApi, + stateApi: StateManagementConfig['api'], + inspectorApi: LensInspectorAdapters, + isTextBasedLanguage: (currentState: LensRuntimeState) => boolean, + startDependencies: LensEmbeddableStartServices, + parentApi?: unknown +): { + api: HasSupportedTriggers & + PublishesDisabledActionIds & + HasEditCapabilities & + PublishesViewMode & { uuid: string }; + comparators: {}; + serialize: () => {}; + cleanup: () => void; +} { + const supportedTriggers = getSupportedTriggers(getState, startDependencies.visualizationMap); + + const isESQLModeEnabled = () => uiSettings.get(ENABLE_ESQL); + + const [viewMode$] = buildObservableVariable<ViewMode>( + extractInheritedViewModeObservable(parentApi) + ); + + const { disabledActionIds, setDisabledActionIds } = apiPublishesDisabledActionIds(parentApi) + ? parentApi + : { disabledActionIds: undefined, setDisabledActionIds: noop }; + const [disabledActionIds$, disabledActionIdsComparator] = buildObservableVariable< + string[] | undefined + >(disabledActionIds); + + if (isTextBasedLanguage(initialState)) { + // do not expose the drilldown action for ES|QL + disabledActionIds$.next(disabledActionIds$.getValue()?.concat(['OPEN_FLYOUT_ADD_DRILLDOWN'])); + } + + /** + * Inline editing section + */ + const navigateToLensEditor = + (stateTransfer: EmbeddableStateTransfer, skipAppLeave?: boolean) => async () => { + if (!parentApi || !apiHasAppContext(parentApi)) { + return; + } + const parentApiContext = parentApi.getAppContext(); + const currentState = getState(); + await stateTransfer.navigateToEditor(APP_ID, { + path: getEditPath(currentState.savedObjectId), + state: { + embeddableId: uuid, + valueInput: currentState, + originatingApp: parentApiContext.currentAppId ?? 'dashboards', + originatingPath: parentApiContext.getCurrentPath?.(), + searchSessionId: currentState.searchSessionId, + }, + skipAppLeave, + }); + }; + + const panelManagementApi = setupPanelManagement(uuid, parentApi, { + isNewlyCreated$: internalApi.isNewlyCreated$, + setAsCreated: internalApi.setAsCreated, + }); + + const updateState = (newState: Pick<LensRuntimeState, 'attributes' | 'savedObjectId'>) => { + stateApi.updateAttributes(newState.attributes); + stateApi.updateSavedObjectId(newState.savedObjectId); + }; + + const openInlineEditor = prepareInlineEditPanel( + initialState, + getState, + updateState, + internalApi, + panelManagementApi, + inspectorApi, + startDependencies, + navigateToLensEditor, + uuid + ); + + /** + * The rest of the edit stuff + */ + const { uiSettings, capabilities, data } = startDependencies; + + const canEdit = () => { + if (viewMode$.getValue() !== 'edit') { + return false; + } + // check if it's in ES|QL mode + if (isTextBasedLanguage(getState()) && !isESQLModeEnabled()) { + return false; + } + return ( + Boolean(capabilities.visualize.save) || + (!getState().savedObjectId && + Boolean(capabilities.dashboard?.showWriteControls) && + Boolean(capabilities.visualize.show)) + ); + }; + + // this will force the embeddable to toggle the inline editing feature + const canEditInline = apiPublishesInlineEditingCapabilities(parentApi) + ? parentApi.canEditInline + : true; + + return { + comparators: { disabledActionIds: disabledActionIdsComparator }, + serialize: emptySerializer, + cleanup: noop, + api: { + uuid, + viewMode: viewMode$, + getTypeDisplayName: () => + i18n.translate('xpack.lens.embeddableDisplayName', { + defaultMessage: 'Lens', + }), + supportedTriggers, + disabledActionIds: disabledActionIds$, + setDisabledActionIds, + + /** + * This is the key method to enable the new Editing capabilities API + * Lens will leverage the netural nature of this function to build the inline editing experience + */ + onEdit: async () => { + if (!parentApi || !apiHasAppContext(parentApi)) { + return; + } + // just navigate directly to the editor + if (!canEditInline) { + const navigateFn = navigateToLensEditor( + new EmbeddableStateTransfer( + startDependencies.coreStart.application.navigateToApp, + startDependencies.coreStart.application.currentAppId$ + ), + true + ); + return navigateFn(); + } + + // save the initial state in case it needs to revert later on + const firstState = getState(); + + const rootEmbeddable = parentApi; + const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined; + const ConfigPanel = await openInlineEditor({ + onApply: (attributes: LensRuntimeState['attributes']) => + updateState({ ...getState(), attributes }), + // restore the first state found when the panel opened + onCancel: () => updateState({ ...firstState }), + }); + if (ConfigPanel) { + mountInlineEditPanel(ConfigPanel, startDependencies.coreStart, overlayTracker, uuid); + } + }, + /** + * Check everything here: user/app permissions and the current inline editing state + */ + isEditingEnabled: () => { + return apiHasAppContext(parentApi) && canEdit() && panelManagementApi.isEditingEnabled(); + }, + getEditHref: async () => { + if (!parentApi || !apiHasAppContext(parentApi)) { + return; + } + const currentState = getState(); + return getEditPath( + currentState.savedObjectId, + currentState.timeRange, + currentState.filters, + data.query.timefilter.timefilter.getRefreshInterval() + ); + }, + }, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_inspector.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_inspector.ts new file mode 100644 index 0000000000000..733a1d4eac46c --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_inspector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noop } from 'lodash'; +import { BehaviorSubject } from 'rxjs'; +import type { Adapters } from '@kbn/inspector-plugin/public'; +import { getLensInspectorService } from '../../lens_inspector_service'; +import { emptySerializer } from '../helper'; +import type { LensEmbeddableStartServices, LensInspectorAdapters } from '../types'; + +export function initializeInspector(services: LensEmbeddableStartServices): { + api: LensInspectorAdapters; + comparators: {}; + serialize: () => {}; + cleanup: () => void; +} { + const inspectorApi = getLensInspectorService(services.inspector); + + return { + api: { + ...inspectorApi, + adapters$: new BehaviorSubject<Adapters>(inspectorApi.getInspectorAdapters()), + }, + comparators: {}, + serialize: emptySerializer, + cleanup: noop, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_integrations.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_integrations.ts new file mode 100644 index 0000000000000..c3501bdfcafb9 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_integrations.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 { + getAggregateQueryMode, + getLanguageDisplayName, + isOfAggregateQueryType, +} from '@kbn/es-query'; +import { noop } from 'lodash'; +import type { HasSerializableState } from '@kbn/presentation-containers'; +import { emptySerializer, isTextBasedLanguage } from '../helper'; +import type { GetStateType, LensEmbeddableStartServices } from '../types'; +import type { IntegrationCallbacks } from '../types'; + +export function initializeIntegrations( + getLatestState: GetStateType, + { attributeService }: LensEmbeddableStartServices +): { + api: Omit< + IntegrationCallbacks, + | 'updateState' + | 'updateAttributes' + | 'updateDataViews' + | 'updateSavedObjectId' + | 'updateOverrides' + | 'updateDataLoading' + | 'getTriggerCompatibleActions' + > & + HasSerializableState; + cleanup: () => void; + serialize: () => {}; + comparators: {}; +} { + return { + api: { + serializeState: () => { + const currentState = getLatestState(); + return attributeService.extractReferences(currentState); + }, + // TODO: workout why we have this duplicated + getFullAttributes: () => getLatestState().attributes, + getSavedVis: () => getLatestState().attributes, + isTextBasedLanguage: () => isTextBasedLanguage(getLatestState()), + getTextBasedLanguage: () => { + const query = getLatestState().attributes?.state.query; + if (!query || !isOfAggregateQueryType(query)) { + return; + } + const language = getAggregateQueryMode(query); + return getLanguageDisplayName(language).toUpperCase(); + }, + }, + comparators: {}, + serialize: emptySerializer, + cleanup: noop, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_internal_api.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_internal_api.ts new file mode 100644 index 0000000000000..2bdc00b3124a2 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_internal_api.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject } from 'rxjs'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { buildObservableVariable, createEmptyLensState } from '../helper'; +import type { + ExpressionWrapperProps, + LensInternalApi, + LensOverrides, + LensRuntimeState, +} from '../types'; +import { apiHasAbortController } from '../type_guards'; +import type { UserMessage } from '../../types'; + +export function initializeInternalApi( + initialState: LensRuntimeState, + parentApi: unknown +): LensInternalApi { + const [hasRenderCompleted$] = buildObservableVariable<boolean>(false); + const [expressionParams$] = buildObservableVariable<ExpressionWrapperProps | null>(null); + const expressionAbortController$ = new BehaviorSubject<AbortController | undefined>(undefined); + if (apiHasAbortController(parentApi)) { + expressionAbortController$.next(parentApi.abortController); + } + const [renderCount$] = buildObservableVariable<number>(0); + + const attributes$ = new BehaviorSubject<LensRuntimeState['attributes']>( + initialState.attributes || createEmptyLensState().attributes + ); + const overrides$ = new BehaviorSubject(initialState.overrides); + const disableTriggers$ = new BehaviorSubject(initialState.disableTriggers); + const dataLoading$ = new BehaviorSubject<boolean | undefined>(undefined); + + const dataViews$ = new BehaviorSubject<DataView[] | undefined>(undefined); + // This is an internal error state, not to be confused with the runtime error state thrown by the expression pipeline + // In both cases a blocking error can happen, but for Lens validation errors we want to have full control over the UI + // while for runtime errors the error will bubble up to the embeddable presentation layer + const validationMessages$ = new BehaviorSubject<UserMessage[]>([]); + // This other set of messages is for non-blocking messages that can be displayed in the UI + const messages$ = new BehaviorSubject<UserMessage[]>([]); + + // This should settle the thing once and for all + // the isNewPanel won't be serialized so it will be always false after the edit panel closes applying the changes + const isNewlyCreated$ = new BehaviorSubject<boolean>(initialState.isNewPanel || false); + + // No need to expose anything at public API right now, that would happen later on + // where each initializer will pick what it needs and publish it + return { + attributes$, + overrides$, + disableTriggers$, + dataLoading$, + hasRenderCompleted$, + expressionParams$, + expressionAbortController$, + renderCount$, + isNewlyCreated$, + dataViews: dataViews$, + dispatchError: () => { + hasRenderCompleted$.next(true); + renderCount$.next(renderCount$.getValue() + 1); + }, + dispatchRenderStart: () => hasRenderCompleted$.next(false), + dispatchRenderComplete: () => { + renderCount$.next(renderCount$.getValue() + 1); + hasRenderCompleted$.next(true); + }, + updateExpressionParams: (newParams: ExpressionWrapperProps | null) => + expressionParams$.next(newParams), + updateDataLoading: (newDataLoading: boolean | undefined) => dataLoading$.next(newDataLoading), + updateOverrides: (overrides: LensOverrides['overrides']) => overrides$.next(overrides), + updateAttributes: (attributes: LensRuntimeState['attributes']) => attributes$.next(attributes), + updateAbortController: (abortController: AbortController | undefined) => + expressionAbortController$.next(abortController), + updateDataViews: (dataViews: DataView[] | undefined) => dataViews$.next(dataViews), + messages$, + updateMessages: (newMessages: UserMessage[]) => messages$.next(newMessages), + validationMessages$, + updateValidationMessages: (newMessages: UserMessage[]) => validationMessages$.next(newMessages), + resetAllMessages: () => { + messages$.next([]); + validationMessages$.next([]); + }, + setAsCreated: () => isNewlyCreated$.next(false), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_search_context.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_search_context.ts new file mode 100644 index 0000000000000..1a608de11e230 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_search_context.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter, Query, AggregateQuery } from '@kbn/es-query'; +import { + PublishesUnifiedSearch, + StateComparators, + getUnchangingComparator, + initializeTimeRange, +} from '@kbn/presentation-publishing'; +import { noop } from 'lodash'; +import { + PublishesSearchSession, + apiPublishesSearchSession, +} from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; +import { buildObservableVariable } from '../helper'; +import { LensInternalApi, LensRuntimeState, LensUnifiedSearchContext } from '../types'; + +export function initializeSearchContext( + initialState: LensRuntimeState, + internalApi: LensInternalApi, + parentApi: unknown +): { + api: PublishesUnifiedSearch & PublishesSearchSession; + comparators: StateComparators<LensUnifiedSearchContext>; + serialize: () => LensUnifiedSearchContext; + cleanup: () => void; +} { + const [searchSessionId$] = buildObservableVariable<string | undefined>( + apiPublishesSearchSession(parentApi) ? parentApi.searchSessionId$ : undefined + ); + + const attributes = internalApi.attributes$.getValue(); + + const [lastReloadRequestTime] = buildObservableVariable<number | undefined>(undefined); + + const [filters$] = buildObservableVariable<Filter[] | undefined>(attributes.state.filters); + + const [query$] = buildObservableVariable<Query | AggregateQuery | undefined>( + attributes.state.query + ); + + const [timeslice$] = buildObservableVariable<[number, number] | undefined>(undefined); + + const timeRange = initializeTimeRange(initialState); + return { + api: { + searchSessionId$, + filters$, + query$, + timeslice$, + isCompatibleWithUnifiedSearch: () => true, + ...timeRange.api, + }, + comparators: { + query: getUnchangingComparator<LensUnifiedSearchContext, 'query'>(), + filters: getUnchangingComparator<LensUnifiedSearchContext, 'filters'>(), + timeslice: getUnchangingComparator<LensUnifiedSearchContext, 'timeslice'>(), + searchSessionId: getUnchangingComparator<LensUnifiedSearchContext, 'searchSessionId'>(), + lastReloadRequestTime: getUnchangingComparator< + LensUnifiedSearchContext, + 'lastReloadRequestTime' + >(), + ...timeRange.comparators, + }, + cleanup: noop, + serialize: () => ({ + searchSessionId: searchSessionId$.getValue(), + filters: filters$.getValue(), + query: query$.getValue(), + timeslice: timeslice$.getValue(), + lastReloadRequestTime: lastReloadRequestTime.getValue(), + ...timeRange.serialize(), + }), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_state_management.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_state_management.ts new file mode 100644 index 0000000000000..af5ecddecd2b4 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_state_management.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + getUnchangingComparator, + type PublishesBlockingError, + type PublishesDataLoading, + type PublishesDataViews, + type PublishesSavedObjectId, + type StateComparators, +} from '@kbn/presentation-publishing'; +import { noop } from 'lodash'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { BehaviorSubject } from 'rxjs'; +import type { IntegrationCallbacks, LensInternalApi, LensRuntimeState } from '../types'; +import { buildObservableVariable } from '../helper'; +import { SharingSavedObjectProps } from '../../types'; + +export interface StateManagementConfig { + api: Pick<IntegrationCallbacks, 'updateAttributes' | 'updateSavedObjectId'> & + PublishesSavedObjectId & + PublishesDataViews & + PublishesDataLoading & + PublishesBlockingError; + serialize: () => Pick<LensRuntimeState, 'attributes' | 'savedObjectId'>; + comparators: StateComparators< + Pick<LensRuntimeState, 'attributes' | 'savedObjectId' | 'abortController'> & { + managed?: boolean | undefined; + sharingSavedObjectProps?: SharingSavedObjectProps | undefined; + } + >; + cleanup: () => void; +} + +/** + * Due to inline editing we need something advanced to handle the state + * management at the embeddable level, so here's the initializers for it + */ +export function initializeStateManagement( + initialState: LensRuntimeState, + internalApi: LensInternalApi +): StateManagementConfig { + const [attributes$, attributesComparator] = buildObservableVariable< + LensRuntimeState['attributes'] + >(internalApi.attributes$); + + const [savedObjectId$, savedObjectIdComparator] = buildObservableVariable< + LensRuntimeState['savedObjectId'] + >(initialState.savedObjectId); + + const [dataViews$] = buildObservableVariable<DataView[] | undefined>(internalApi.dataViews); + const [dataLoading$] = buildObservableVariable<boolean | undefined>(internalApi.dataLoading$); + const [abortController$, abortControllerComparator] = buildObservableVariable< + AbortController | undefined + >(internalApi.expressionAbortController$); + + // This is the way to communicate to the embeddable panel to render a blocking error with the + // default panel error component - i.e. cannot find a Lens SO type of thing. + // For Lens specific errors, we use a Lens specific error component. + const [blockingError$] = buildObservableVariable<Error | undefined>(undefined); + return { + api: { + updateAttributes: internalApi.updateAttributes, + updateSavedObjectId: (newSavedObjectId: LensRuntimeState['savedObjectId']) => + savedObjectId$.next(newSavedObjectId), + savedObjectId: savedObjectId$, + dataViews: dataViews$, + dataLoading: dataLoading$, + blockingError: blockingError$, + }, + serialize: () => { + return { + attributes: attributes$.getValue(), + savedObjectId: savedObjectId$.getValue(), + abortController: abortController$.getValue(), + }; + }, + comparators: { + // need to force cast this to make it pass the type check + // @TODO: workout why this is needed + attributes: attributesComparator as [ + BehaviorSubject<LensRuntimeState['attributes']>, + (newValue: LensRuntimeState['attributes'] | undefined) => void, + ( + a: LensRuntimeState['attributes'] | undefined, + b: LensRuntimeState['attributes'] | undefined + ) => boolean + ], + savedObjectId: savedObjectIdComparator, + abortController: abortControllerComparator, + sharingSavedObjectProps: getUnchangingComparator(), + managed: getUnchangingComparator(), + }, + cleanup: noop, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_visualization_context.ts b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_visualization_context.ts new file mode 100644 index 0000000000000..93d544013e710 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/initializers/initialize_visualization_context.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LensInternalApi, VisualizationContext, VisualizationContextHelper } from '../types'; + +export function initializeVisualizationContext( + internalApi: LensInternalApi +): VisualizationContextHelper { + // TODO: this will likely be merged together with the state$ observable + let visualizationContext: VisualizationContext = { + doc: internalApi.attributes$.getValue(), + mergedSearchContext: {}, + indexPatterns: {}, + indexPatternRefs: [], + activeVisualizationState: undefined, + activeDatasourceState: undefined, + activeData: undefined, + }; + return { + getVisualizationContext: () => visualizationContext, + updateVisualizationContext: (newVisualizationContext: Partial<VisualizationContext>) => { + visualizationContext = { + ...visualizationContext, + ...newVisualizationContext, + }; + }, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/inline_editing/mount.tsx b/x-pack/plugins/lens/public/react_embeddable/inline_editing/mount.tsx new file mode 100644 index 0000000000000..566c5b27b6541 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/inline_editing/mount.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { TracksOverlays } from '@kbn/presentation-containers'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +/** + * Shared logic to mount the inline config panel + * @param ConfigPanel + * @param coreStart + * @param overlayTracker + * @param uuid + * @param container + */ +export function mountInlineEditPanel( + ConfigPanel: JSX.Element, + coreStart: CoreStart, + overlayTracker: TracksOverlays | undefined, + uuid?: string, + container?: HTMLElement | null +) { + if (container) { + ReactDOM.render(ConfigPanel, container); + } else { + const handle = coreStart.overlays.openFlyout( + toMountPoint( + React.cloneElement(ConfigPanel, { + closeFlyout: () => { + overlayTracker?.clearOverlays(); + handle.close(); + }, + }), + coreStart + ), + { + className: 'lnsConfigPanel__overlay', + size: 's', + 'data-test-subj': 'customizeLens', + type: 'push', + paddingSize: 'm', + maxWidth: 800, + hideCloseButton: true, + isResizable: true, + onClose: (overlayRef) => { + overlayTracker?.clearOverlays(); + overlayRef.close(); + }, + outsideClickCloses: true, + } + ); + if (uuid) { + overlayTracker?.openOverlay(handle, { focusedPanelId: uuid }); + } + } +} diff --git a/x-pack/plugins/lens/public/react_embeddable/inline_editing/panel_management.tsx b/x-pack/plugins/lens/public/react_embeddable/inline_editing/panel_management.tsx new file mode 100644 index 0000000000000..5753c8112d876 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/inline_editing/panel_management.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 { apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { BehaviorSubject } from 'rxjs'; +import { PublishingSubject } from '@kbn/presentation-publishing'; +import { LensRuntimeState } from '../types'; + +export interface PanelManagementApi { + isEditingEnabled: () => boolean; + isNewPanel: () => boolean; + onStopEditing: (isCancel: boolean, state: LensRuntimeState | undefined) => void; +} + +export function setupPanelManagement( + uuid: string, + parentApi: unknown, + { + isNewlyCreated$, + setAsCreated, + }: { + isNewlyCreated$: PublishingSubject<boolean>; + setAsCreated: () => void; + } +): PanelManagementApi { + const isEditing$ = new BehaviorSubject(false); + + return { + isEditingEnabled: () => true, + isNewPanel: () => isNewlyCreated$.getValue(), + onStopEditing: (isCancel: boolean = false, state: LensRuntimeState | undefined) => { + isEditing$.next(false); + if (isNewlyCreated$.getValue() && isCancel && !state) { + if (apiIsPresentationContainer(parentApi)) { + parentApi?.removePanel(uuid); + } + } + setAsCreated(); + }, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx b/x-pack/plugins/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx new file mode 100644 index 0000000000000..e37e671132964 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; +import React from 'react'; +import { EditLensConfigurationProps } from '../../app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration'; +import { EditConfigPanelProps } from '../../app_plugin/shared/edit_on_the_fly/types'; +import { getActiveDatasourceIdFromDoc } from '../../utils'; +import { isTextBasedLanguage } from '../helper'; +import { + GetStateType, + LensEmbeddableStartServices, + LensInspectorAdapters, + LensInternalApi, + LensRuntimeState, + TypedLensSerializedState, +} from '../types'; +import { PanelManagementApi } from './panel_management'; +import { getStateManagementForInlineEditing } from './state_management'; + +export function prepareInlineEditPanel( + initialState: LensRuntimeState, + getState: GetStateType, + updateState: (newState: Pick<LensRuntimeState, 'attributes' | 'savedObjectId'>) => void, + { dataLoading$, isNewlyCreated$ }: Pick<LensInternalApi, 'dataLoading$' | 'isNewlyCreated$'>, + panelManagementApi: PanelManagementApi, + inspectorApi: LensInspectorAdapters, + { + coreStart, + ...startDependencies + }: Omit< + LensEmbeddableStartServices, + | 'timefilter' + | 'coreHttp' + | 'capabilities' + | 'expressionRenderer' + | 'documentToExpression' + | 'injectFilterReferences' + | 'visualizationMap' + | 'datasourceMap' + | 'theme' + | 'uiSettings' + | 'attributeService' + >, + navigateToLensEditor?: ( + stateTransfer: EmbeddableStateTransfer, + skipAppLeave?: boolean + ) => () => Promise<void>, + uuid?: string +) { + return async function openConfigPanel({ + onApply, + onCancel, + hideTimeFilterInfo, + }: Partial<Pick<EditConfigPanelProps, 'onApply' | 'onCancel' | 'hideTimeFilterInfo'>> = {}) { + const { getEditLensConfiguration, getVisualizationMap, getDatasourceMap } = await import( + '../../async_services' + ); + const visualizationMap = getVisualizationMap(); + const datasourceMap = getDatasourceMap(); + + const currentState = getState(); + const attributes = currentState.attributes as TypedLensSerializedState['attributes']; + const activeDatasourceId = (getActiveDatasourceIdFromDoc(attributes) || + 'formBased') as EditLensConfigurationProps['datasourceId']; + + const { updatePanelState, updateSuggestion } = getStateManagementForInlineEditing( + activeDatasourceId, + () => getState().attributes as TypedLensSerializedState['attributes'], + (attrs: TypedLensSerializedState['attributes'], resetId: boolean = false) => { + updateState({ + attributes: attrs, + savedObjectId: resetId ? undefined : currentState.savedObjectId, + }); + }, + visualizationMap, + datasourceMap, + startDependencies.data.query.filterManager.extract + ); + + const updateByRefInput = (savedObjectId: LensRuntimeState['savedObjectId']) => { + updateState({ attributes, savedObjectId }); + }; + const Component = await getEditLensConfiguration( + coreStart, + startDependencies, + visualizationMap, + datasourceMap + ); + + if (attributes?.visualizationType == null) { + return null; + } + return ( + <Component + attributes={attributes} + updateByRefInput={updateByRefInput} + updatePanelState={updatePanelState} + updateSuggestion={updateSuggestion} + datasourceId={activeDatasourceId} + lensAdapters={inspectorApi.getInspectorAdapters()} + dataLoading$={dataLoading$} + panelId={uuid} + savedObjectId={currentState.savedObjectId} + navigateToLensEditor={ + !isTextBasedLanguage(currentState) && navigateToLensEditor + ? navigateToLensEditor( + new EmbeddableStateTransfer( + coreStart.application.navigateToApp, + coreStart.application.currentAppId$ + ), + true + ) + : undefined + } + displayFlyoutHeader + canEditTextBasedQuery={isTextBasedLanguage(currentState)} + isNewPanel={panelManagementApi.isNewPanel()} + onCancel={() => { + panelManagementApi.onStopEditing( + true, + // DSL/form based charts are created via the full editor, so there's + // an initial state to preserve. ES|QL charts are created inline, so it needs to pass an empty state + // and the panelManagementApi will decide whether to remove the panel or not + isNewlyCreated$.getValue() ? undefined : initialState + ); + onCancel?.(); + }} + onApply={(newAttributes) => { + panelManagementApi.onStopEditing(false, { ...getState(), attributes: newAttributes }); + if (newAttributes.visualizationType != null) { + onApply?.(newAttributes); + } + }} + hideTimeFilterInfo={hideTimeFilterInfo} + /> + ); + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/inline_editing/state_management.tsx b/x-pack/plugins/lens/public/react_embeddable/inline_editing/state_management.tsx new file mode 100644 index 0000000000000..2a4f1f48fd0dc --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/inline_editing/state_management.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FilterManager } from '@kbn/data-plugin/public'; +import { mergeToNewDoc } from '../../state_management/shared_logic'; +import type { DatasourceStates } from '../../state_management/types'; +import type { VisualizationMap, DatasourceMap } from '../../types'; +import type { TypedLensSerializedState } from '../types'; + +export function getStateManagementForInlineEditing( + activeDatasourceId: 'formBased' | 'textBased', + getAttributes: () => TypedLensSerializedState['attributes'], + updateAttributes: ( + newAttributes: TypedLensSerializedState['attributes'], + resetId?: boolean + ) => void, + visualizationMap: VisualizationMap, + datasourceMap: DatasourceMap, + extractFilterReferences: FilterManager['extract'] +) { + const updatePanelState = ( + datasourceState: unknown, + visualizationState: unknown, + visualizationType?: string + ) => { + const viz = getAttributes(); + const datasourceStates: DatasourceStates = { + [activeDatasourceId]: { + isLoading: false, + state: datasourceState, + }, + }; + const newViz = mergeToNewDoc( + viz, + { + activeId: visualizationType || viz.visualizationType, + state: visualizationState, + }, + datasourceStates, + viz.state.query, + viz.state.filters, + activeDatasourceId, + viz.state.adHocDataViews || {}, + { visualizationMap, datasourceMap, extractFilterReferences } + ); + const newDoc = { + ...viz, + ...newViz, + }; + + if (newDoc.state) { + updateAttributes(newDoc, true); + } + }; + + const updateSuggestion = updateAttributes; + + return { updateSuggestion, updatePanelState }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/lens_embeddable.tsx b/x-pack/plugins/lens/public/react_embeddable/lens_embeddable.tsx new file mode 100644 index 0000000000000..8c17063f97a2e --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/lens_embeddable.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { DOC_TYPE } from '../../common/constants'; +import { + LensApi, + LensEmbeddableStartServices, + LensRuntimeState, + LensSerializedState, +} from './types'; + +import { loadEmbeddableData } from './data_loader'; +import { isTextBasedLanguage, deserializeState } from './helper'; +import { initializeEditApi } from './initializers/initialize_edit'; +import { initializeInspector } from './initializers/initialize_inspector'; +import { initializeDashboardServices } from './initializers/initialize_dashboard_services'; +import { initializeInternalApi } from './initializers/initialize_internal_api'; +import { initializeSearchContext } from './initializers/initialize_search_context'; +import { initializeVisualizationContext } from './initializers/initialize_visualization_context'; +import { initializeActionApi } from './initializers/initialize_actions'; +import { initializeIntegrations } from './initializers/initialize_integrations'; +import { initializeStateManagement } from './initializers/initialize_state_management'; +import { LensEmbeddableComponent } from './renderer/lens_embeddable_component'; + +export const createLensEmbeddableFactory = ( + services: LensEmbeddableStartServices +): ReactEmbeddableFactory<LensSerializedState, LensRuntimeState, LensApi> => { + return { + type: DOC_TYPE, + /** + * This is called before the build and will make sure that the + * final state will contain the attributes object + */ + deserializeState: async ({ rawState, references }) => + deserializeState(services.attributeService, rawState, references), + /** + * This is called after the deserialize, so some assumptions can be made about its arguments: + * @param state the Lens "runtime" state, which means that 'attributes' is always present. + * The difference for a by-value and a by-ref can be determined by the presence of 'savedObjectId' in the state + * @param buildApi a utility function to build the Lens API together to instrument the embeddable container on how to detect + * significative changes in the state (i.e. worth a save or not) + * @param uuid a unique identifier for the embeddable panel + * @param parentApi a set of props passed down from the embeddable container. Note: no assumptions can be made about its content + * so the usage of type-guards is recommended before extracting data from it. + * Due to the new embeddable being rendered by a <ReactEmbeddableRenderer /> wrapper, this is the only way + * to pass data/props from a container. + * Typical use cases is the forwarding of the unifiedSearch context to the embeddable, or the passing props + * from the Lens component container to the Lens embeddable. + * @returns an object with the Lens API and the React component to render in the Embeddable + */ + buildEmbeddable: async (initialState, buildApi, uuid, parentApi) => { + /** + * Observables and functions declared here are used internally to store mutating state values + * This is an internal API not exposed outside of the embeddable. + */ + const internalApi = initializeInternalApi(initialState, parentApi); + + const visualizationContextHelper = initializeVisualizationContext(internalApi); + + /** + * Initialize various configurations required to build all the required + * parts for the Lens embeddable. + * Each initialize call returns an object with the following properties: + * - api: a set of methods or observables (also non-serializable) who can be picked up within the component + * - serialize: a serializable subset of the Lens runtime state + * - comparators: a set of comparators to help Dashboard determine if the state has changed since its saved state + * - cleanup: a function to clean up any resources when the component is unmounted + * + * Mind: the getState argument is ok to pass as long as it is lazy evaluated (i.e. called within a function). + * If there's something that should be immediately computed use the "initialState" deserialized variable. + */ + const stateConfig = initializeStateManagement(initialState, internalApi); + const dashboardConfig = initializeDashboardServices( + initialState, + getState, + internalApi, + stateConfig, + parentApi, + services + ); + + const inspectorConfig = initializeInspector(services); + + const editConfig = initializeEditApi( + uuid, + initialState, + getState, + internalApi, + stateConfig.api, + inspectorConfig.api, + isTextBasedLanguage, + services, + parentApi + ); + + const searchContextConfig = initializeSearchContext(initialState, internalApi, parentApi); + const integrationsConfig = initializeIntegrations(getState, services); + const actionsConfig = initializeActionApi( + uuid, + initialState, + getState, + parentApi, + searchContextConfig.api, + dashboardConfig.api, + visualizationContextHelper, + services + ); + + /** + * This is useful to have always the latest version of the state + * at hand when calling callbacks or performing actions + */ + function getState(): LensRuntimeState { + return { + ...actionsConfig.serialize(), + ...editConfig.serialize(), + ...inspectorConfig.serialize(), + ...dashboardConfig.serialize(), + ...searchContextConfig.serialize(), + ...integrationsConfig.serialize(), + ...stateConfig.serialize(), + }; + } + + /** + * Lens API is the object that can be passed to the final component/renderer and + * provide access to the services for and by the outside world + */ + const api: LensApi = buildApi( + // Note: the order matters here, so make sure to have the + // dashboardConfig who owns the savedObjectId after the + // stateConfig one who owns the inline editing + { + ...editConfig.api, + ...inspectorConfig.api, + ...searchContextConfig.api, + ...actionsConfig.api, + ...integrationsConfig.api, + ...stateConfig.api, + ...dashboardConfig.api, + }, + { + ...stateConfig.comparators, + ...editConfig.comparators, + ...inspectorConfig.comparators, + ...searchContextConfig.comparators, + ...actionsConfig.comparators, + ...integrationsConfig.comparators, + ...dashboardConfig.comparators, + } + ); + + // Compute the expression using the provided parameters + // Inside a subscription will be updated based on each unifiedSearch change + // and as side effect update few observables as expressionParams$, expressionAbortController$ and renderCount$ with the new values upon updates + const expressionConfig = loadEmbeddableData( + uuid, + getState, + api, + parentApi, + internalApi, + services, + visualizationContextHelper + ); + + const onUnmount = () => { + editConfig.cleanup(); + inspectorConfig.cleanup(); + searchContextConfig.cleanup(); + expressionConfig.cleanup(); + actionsConfig.cleanup(); + integrationsConfig.cleanup(); + dashboardConfig.cleanup(); + }; + + return { + api, + Component: () => ( + <LensEmbeddableComponent api={api} internalApi={internalApi} onUnmount={onUnmount} /> + ), + }; + }, + }; +}; diff --git a/x-pack/plugins/lens/public/react_embeddable/logger.ts b/x-pack/plugins/lens/public/react_embeddable/logger.ts new file mode 100644 index 0000000000000..05454843b6819 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/logger.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. + */ + +/** + * Conditional (window.ELASTIC_LENS_LOGGER needs to be set to true) logger function + * @param message - mandatory message to log + * @param payload - optional object to log + */ + +export const addLog = (message: string, payload?: unknown) => { + // @ts-expect-error + const logger = window?.ELASTIC_LENS_LOGGER; + + if (logger) { + if (logger === 'debug') { + // eslint-disable-next-line no-console + console.log(`[Lens] ${message}`, payload); + } else { + // eslint-disable-next-line no-console + console.log(`[Lens] ${message}`); + } + } +}; diff --git a/x-pack/plugins/lens/public/react_embeddable/mocks/index.tsx b/x-pack/plugins/lens/public/react_embeddable/mocks/index.tsx new file mode 100644 index 0000000000000..a3992e504c4df --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/mocks/index.tsx @@ -0,0 +1,352 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject, Subject } from 'rxjs'; +import deepMerge from 'deepmerge'; +import React from 'react'; +import faker from 'faker'; +import { Query, Filter, AggregateQuery, TimeRange } from '@kbn/es-query'; +import { PhaseEvent, ViewMode } from '@kbn/presentation-publishing'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { Adapters } from '@kbn/inspector-plugin/common'; +import { coreMock } from '@kbn/core/public/mocks'; +import { visualizationsPluginMock } from '@kbn/visualizations-plugin/public/mocks'; +import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { ReactExpressionRendererProps } from '@kbn/expressions-plugin/public'; +import { ReactEmbeddableDynamicActionsApi } from '@kbn/embeddable-enhanced-plugin/public/plugin'; +import { DOC_TYPE } from '../../../common/constants'; +import { createEmptyLensState } from '../helper'; +import { + ExpressionWrapperProps, + LensApi, + LensEmbeddableStartServices, + LensInternalApi, + LensRendererProps, + LensRuntimeState, + LensSerializedState, + VisualizationContext, +} from '../types'; +import { + createMockDatasource, + createMockVisualization, + defaultDoc, + makeDefaultServices, +} from '../../mocks'; +import { + Datasource, + DatasourceMap, + UserMessage, + Visualization, + VisualizationMap, +} from '../../types'; + +const LensApiMock: LensApi = { + // Static props + type: DOC_TYPE, + uuid: faker.random.uuid(), + // Shared Embeddable Observables + panelTitle: new BehaviorSubject<string | undefined>(faker.lorem.words()), + hidePanelTitle: new BehaviorSubject<boolean | undefined>(false), + filters$: new BehaviorSubject<Filter[] | undefined>([]), + query$: new BehaviorSubject<Query | AggregateQuery | undefined>({ + query: 'test', + language: 'kuery', + }), + timeRange$: new BehaviorSubject<TimeRange | undefined>({ from: 'now-15m', to: 'now' }), + dataLoading: new BehaviorSubject<boolean | undefined>(false), + // Methods + getSavedVis: jest.fn(), + getFullAttributes: jest.fn(), + canViewUnderlyingData$: new BehaviorSubject<boolean>(false), + loadViewUnderlyingData: jest.fn(), + getViewUnderlyingDataArgs: jest.fn(() => ({ + dataViewSpec: { id: 'index-pattern-id' }, + timeRange: { from: 'now-7d', to: 'now' }, + filters: [], + query: undefined, + columns: [], + })), + isTextBasedLanguage: jest.fn(() => true), + getTextBasedLanguage: jest.fn(), + getInspectorAdapters: jest.fn(() => ({})), + inspect: jest.fn(), + closeInspector: jest.fn(async () => {}), + supportedTriggers: jest.fn(() => []), + canLinkToLibrary: jest.fn(async () => false), + canUnlinkFromLibrary: jest.fn(async () => false), + unlinkFromLibrary: jest.fn(), + checkForDuplicateTitle: jest.fn(), + /** New embeddable api inherited methods */ + resetUnsavedChanges: jest.fn(), + serializeState: jest.fn(), + snapshotRuntimeState: jest.fn(), + saveToLibrary: jest.fn(async () => 'saved-id'), + getByValueRuntimeSnapshot: jest.fn(), + onEdit: jest.fn(), + isEditingEnabled: jest.fn(() => true), + getTypeDisplayName: jest.fn(() => 'Lens'), + setPanelTitle: jest.fn(), + setHidePanelTitle: jest.fn(), + phase$: new BehaviorSubject<PhaseEvent | undefined>({ + id: faker.random.uuid(), + status: 'rendered', + timeToEvent: 1000, + }), + unsavedChanges: new BehaviorSubject<object | undefined>(undefined), + dataViews: new BehaviorSubject<DataView[] | undefined>(undefined), + libraryId$: new BehaviorSubject<string | undefined>(undefined), + savedObjectId: new BehaviorSubject<string | undefined>(undefined), + adapters$: new BehaviorSubject<Adapters>({}), + updateAttributes: jest.fn(), + updateSavedObjectId: jest.fn(), + updateOverrides: jest.fn(), + getByReferenceState: jest.fn(), + getByValueState: jest.fn(), + getTriggerCompatibleActions: jest.fn(), + blockingError: new BehaviorSubject<Error | undefined>(undefined), + panelDescription: new BehaviorSubject<string | undefined>(undefined), + setPanelDescription: jest.fn(), + viewMode: new BehaviorSubject<ViewMode>('view'), + disabledActionIds: new BehaviorSubject<string[] | undefined>(undefined), + setDisabledActionIds: jest.fn(), +}; + +const LensSerializedStateMock: LensSerializedState = createEmptyLensState( + 'lnsXY', + faker.lorem.words(), + faker.lorem.text(), + { query: 'test', language: 'kuery' } +); + +export function getLensAttributesMock(attributes?: Partial<LensRuntimeState['attributes']>) { + return deepMerge(LensSerializedStateMock.attributes!, attributes ?? {}); +} + +export function getLensApiMock(overrides: Partial<LensApi> = {}) { + return { + ...LensApiMock, + ...overrides, + }; +} + +export function getLensSerializedStateMock(overrides: Partial<LensSerializedState> = {}) { + return { + savedObjectId: faker.random.uuid(), + ...LensSerializedStateMock, + ...overrides, + }; +} + +export function getLensRuntimeStateMock( + overrides: Partial<LensRuntimeState> = {} +): LensRuntimeState { + return { + ...(LensSerializedStateMock as LensRuntimeState), + ...overrides, + }; +} + +export function getLensComponentProps(overrides: Partial<LensRendererProps> = {}) { + return { + ...LensSerializedStateMock, + ...LensApiMock, + ...overrides, + }; +} + +export function makeEmbeddableServices( + sessionIdSubject = new Subject<string>(), + sessionId: string | undefined = undefined, + { + visOverrides, + dataOverrides, + }: { + visOverrides?: { id: string } & Partial<Visualization>; + dataOverrides?: { id: string } & Partial<Datasource>; + } = {} +): jest.Mocked<LensEmbeddableStartServices> { + const services = makeDefaultServices(sessionIdSubject, sessionId); + return { + ...services, + expressions: expressionsPluginMock.createStartContract(), + visualizations: visualizationsPluginMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + eventAnnotation: {} as LensEmbeddableStartServices['eventAnnotation'], + timefilter: services.data.query.timefilter.timefilter, + coreHttp: services.http, + coreStart: coreMock.createStart(), + capabilities: services.application.capabilities, + expressionRenderer: jest.fn().mockReturnValue(null), + documentToExpression: jest.fn(), + injectFilterReferences: services.data.query.filterManager.inject as jest.Mock, + visualizationMap: mockVisualizationMap(visOverrides?.id, visOverrides), + datasourceMap: mockDatasourceMap(dataOverrides?.id, dataOverrides), + charts: chartPluginMock.createStartContract(), + inspector: { + ...services.inspector, + isAvailable: jest.fn().mockReturnValue(true), + open: jest.fn(), + }, + uiActions: { + ...services.uiActions, + getTrigger: jest.fn().mockImplementation(() => ({ exec: jest.fn() })), + }, + embeddableEnhanced: { + initializeReactEmbeddableDynamicActions: jest.fn( + () => + ({ + dynamicActionsApi: { + enhancements: { dynamicActions: {} }, + setDynamicActions: jest.fn(), + dynamicActionsState$: {}, + }, + dynamicActionsComparator: jest.fn(), + serializeDynamicActions: jest.fn(), + startDynamicActions: jest.fn(), + } as unknown as ReactEmbeddableDynamicActionsApi) + ), + }, + }; +} + +export const mockVisualizationMap = ( + type: string | undefined = undefined, + overrides: Partial<Visualization> = {} +): VisualizationMap => { + if (type == null) { + return {}; + } + return { + [type]: { ...createMockVisualization(type), ...overrides }, + }; +}; + +export const mockDatasourceMap = ( + type: string | undefined = undefined, + overrides: Partial<Datasource> = {} +): DatasourceMap => { + const baseMap = { + // define the existing ones + formBased: createMockDatasource('formBased'), + textBased: createMockDatasource('textBased'), + }; + if (type == null) { + return baseMap; + } + return { + // define the existing ones + ...baseMap, + // override at will + [type]: { + ...createMockDatasource(type), + ...overrides, + }, + }; +}; + +export function createExpressionRendererMock(): jest.Mock< + React.ReactElement, + [ReactExpressionRendererProps] +> { + return jest.fn(({ expression }) => ( + <span data-test-subj="lnsExpressionRenderer"> + {(expression as string) || 'Expression renderer mock'} + </span> + )); +} + +function getValidExpressionParams( + overrides: Partial<ExpressionWrapperProps> = {} +): ExpressionWrapperProps { + return { + ExpressionRenderer: createExpressionRendererMock(), + expression: 'test', + searchContext: {}, + handleEvent: jest.fn(), + onData$: jest.fn(), + onRender$: jest.fn(), + addUserMessages: jest.fn(), + onRuntimeError: jest.fn(), + lensInspector: { + getInspectorAdapters: jest.fn(), + inspect: jest.fn(), + closeInspector: jest.fn(), + }, + ...overrides, + }; +} + +const LensInternalApiMock: LensInternalApi = { + dataViews: new BehaviorSubject<DataView[] | undefined>(undefined), + attributes$: new BehaviorSubject<LensRuntimeState['attributes']>(defaultDoc), + overrides$: new BehaviorSubject<LensRuntimeState['overrides']>(undefined), + disableTriggers$: new BehaviorSubject<LensRuntimeState['disableTriggers']>(undefined), + dataLoading$: new BehaviorSubject<boolean | undefined>(undefined), + hasRenderCompleted$: new BehaviorSubject<boolean>(true), + expressionParams$: new BehaviorSubject<ExpressionWrapperProps | null>(getValidExpressionParams()), + expressionAbortController$: new BehaviorSubject<AbortController | undefined>(undefined), + renderCount$: new BehaviorSubject<number>(0), + messages$: new BehaviorSubject<UserMessage[]>([]), + validationMessages$: new BehaviorSubject<UserMessage[]>([]), + isNewlyCreated$: new BehaviorSubject<boolean>(true), + updateAttributes: jest.fn(), + updateOverrides: jest.fn(), + dispatchRenderStart: jest.fn(), + dispatchRenderComplete: jest.fn(), + updateDataLoading: jest.fn(), + updateExpressionParams: jest.fn(), + updateAbortController: jest.fn(), + updateDataViews: jest.fn(), + updateMessages: jest.fn(), + resetAllMessages: jest.fn(), + dispatchError: jest.fn(), + updateValidationMessages: jest.fn(), + setAsCreated: jest.fn(), +}; + +export function getLensInternalApiMock(overrides: Partial<LensInternalApi> = {}): LensInternalApi { + return { + ...LensInternalApiMock, + ...overrides, + }; +} + +export function getVisualizationContextHelperMock( + attributesOverrides?: Partial<LensRuntimeState['attributes']>, + contextOverrides?: Omit<Partial<VisualizationContext>, 'doc'> +) { + return { + getVisualizationContext: jest.fn(() => ({ + mergedSearchContext: {}, + indexPatterns: {}, + indexPatternRefs: [], + activeVisualizationState: undefined, + activeDatasourceState: undefined, + activeData: undefined, + ...contextOverrides, + doc: getLensAttributesMock(attributesOverrides), + })), + updateVisualizationContext: jest.fn(), + }; +} + +export function createUnifiedSearchApi( + query: Query | AggregateQuery = { + query: '', + language: 'kuery', + }, + filters: Filter[] = [], + timeRange: TimeRange = { from: 'now-7d', to: 'now' } +) { + return { + filters$: new BehaviorSubject<Filter[] | undefined>(filters), + query$: new BehaviorSubject<Query | AggregateQuery | undefined>(query), + timeRange$: new BehaviorSubject<TimeRange | undefined>(timeRange), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/renderer/hooks.ts b/x-pack/plugins/lens/public/react_embeddable/renderer/hooks.ts new file mode 100644 index 0000000000000..c6d97d16ad386 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/renderer/hooks.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 { partition } from 'lodash'; +import { useEffect, useMemo, useRef } from 'react'; +import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; +import { dispatchRenderComplete, dispatchRenderStart } from '@kbn/kibana-utils-plugin/public'; +import { LensApi, LensInternalApi } from '../types'; + +/** + * This hooks known how to extract message based on types for the UI + */ +export function useMessages({ messages$ }: LensInternalApi) { + const latestMessages = useStateFromPublishingSubject(messages$); + return useMemo( + () => partition(latestMessages, ({ severity }) => severity !== 'info'), + [latestMessages] + ); +} + +/** + * This hook is responsible to emit the render start/complete JS event + * The render error is handled by the data_loader itself when updating the blocking errors + */ +export function useDispatcher(hasRendered: boolean, api: LensApi) { + const rootRef = useRef<HTMLDivElement | null>(null); + useEffect(() => { + if (!rootRef.current || api.blockingError?.getValue()) { + return; + } + if (hasRendered) { + dispatchRenderComplete(rootRef.current); + } else { + dispatchRenderStart(rootRef.current); + } + }, [hasRendered, api.blockingError, rootRef]); + return rootRef; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx new file mode 100644 index 0000000000000..5bc55d43c3212 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; +import { useSearchApi } from '@kbn/presentation-publishing'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import type { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; +import type { LensApi, LensRendererProps, LensRuntimeState, LensSerializedState } from '../types'; +import { LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; +import { createEmptyLensState } from '../helper'; + +// This little utility uses the same pattern of the useSearchApi hook: +// create the Subject once and then update its value on change +function useObservableVariable<T extends unknown>(value: T) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const observable = useMemo(() => new BehaviorSubject<T>(value), []); + + // update the observable on change + useEffect(() => { + observable.next(value); + }, [observable, value]); + + return observable; +} + +type PanelProps = Pick< + PresentationPanelProps<LensApi>, + | 'showShadow' + | 'showBorder' + | 'showBadges' + | 'showNotifications' + | 'hideLoader' + | 'hideHeader' + | 'hideInspector' + | 'getActions' +>; + +/** + * The aim of this component is to provide a wrapper for other plugins who want to + * use a Lens component into their own page. This hides the embeddable parts of it + * by wrapping it into a ReactEmbeddableRenderer component and exposing a custom API + */ +export function LensRenderer({ + title, + withDefaultActions, + extraActions, + showInspector, + syncColors, + syncCursor, + syncTooltips, + viewMode, + id, + query, + filters, + timeRange, + disabledActions, + ...props +}: LensRendererProps) { + // Use the settings interface to store panel settings + const settings = useMemo(() => { + return { + syncColors$: new BehaviorSubject(false), + syncCursor$: new BehaviorSubject(false), + syncTooltips$: new BehaviorSubject(false), + }; + }, []); + const disabledActionIds$ = useObservableVariable(disabledActions); + const viewMode$ = useObservableVariable(viewMode); + + // Lens API will be set once, but when set trigger a reflow to adopt the latest attributes + const [lensApi, setLensApi] = useState<LensApi | undefined>(undefined); + const initialStateRef = useRef<LensSerializedState>( + props.attributes ? { attributes: props.attributes } : createEmptyLensState(null, title) + ); + + const searchApi = useSearchApi({ query, filters, timeRange }); + + const showPanelChrome = Boolean(withDefaultActions) || (extraActions?.length || 0) > 0; + + // Re-render on changes + // internally the embeddable will evaluate whether it is worth to actual render or not + useEffect(() => { + // trigger a re-render if the attributes change + if (lensApi) { + lensApi.updateAttributes({ + ...('attributes' in initialStateRef.current + ? initialStateRef.current.attributes + : initialStateRef.current), + ...props.attributes, + }); + lensApi.updateOverrides(props.overrides); + } + }, [lensApi, props.attributes, props.overrides]); + + useEffect(() => { + if (syncColors != null && settings.syncColors$.getValue() !== syncColors) { + settings.syncColors$.next(syncColors); + } + if (syncCursor != null && settings.syncCursor$.getValue() !== syncCursor) { + settings.syncCursor$.next(syncCursor); + } + if (syncTooltips != null && settings.syncTooltips$.getValue() !== syncTooltips) { + settings.syncTooltips$.next(syncTooltips); + } + }, [settings, syncColors, syncCursor, syncTooltips]); + + const panelProps: PanelProps = useMemo(() => { + return { + hideInspector: !showInspector, + hideHeader: showPanelChrome, + showNotifications: false, + showShadow: false, + showBadges: false, + getActions: async (triggerId, context) => { + const actions = withDefaultActions + ? await lensApi?.getTriggerCompatibleActions(triggerId, context) + : []; + + return (extraActions ?? []).concat(actions || []); + }, + }; + }, [showInspector, showPanelChrome, withDefaultActions, extraActions, lensApi]); + + return ( + <ReactEmbeddableRenderer<LensSerializedState, LensRuntimeState, LensApi> + type={LENS_EMBEDDABLE_TYPE} + maybeId={id} + getParentApi={() => ({ + // forward the Lens components to the embeddable + ...props, + // forward the unified search context + ...searchApi, + disabledActionIds: disabledActionIds$, + setDisabledActionIds: (ids: string[] | undefined) => disabledActionIds$.next(ids), + viewMode: viewMode$, + // pass the sync* settings with the unified settings interface + settings, + // make sure to provide the initial state (useful for the comparison check) + getSerializedStateForChild: () => ({ rawState: initialStateRef.current, references: [] }), + // update the runtime state on changes + getRuntimeStateForChild: () => ({ + ...initialStateRef.current, + attributes: props.attributes, + }), + })} + onApiAvailable={setLensApi} + hidePanelChrome={!showPanelChrome} + panelProps={panelProps} + /> + ); +} + +export type EmbeddableComponent = React.ComponentType<LensRendererProps>; diff --git a/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx new file mode 100644 index 0000000000000..04c3511ab3d4f --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx @@ -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 { render, screen } from '@testing-library/react'; +import { getLensApiMock, getLensInternalApiMock } from '../mocks'; +import { LensApi, LensInternalApi } from '../types'; +import { BehaviorSubject } from 'rxjs'; +import { PublishingSubject } from '@kbn/presentation-publishing'; +import React from 'react'; +import { LensEmbeddableComponent } from './lens_embeddable_component'; + +type GetValueType<Type> = Type extends PublishingSubject<infer X> ? X : never; + +function getDefaultProps({ + internalApiOverrides = undefined, + apiOverrides = undefined, +}: { internalApiOverrides?: Partial<LensInternalApi>; apiOverrides?: Partial<LensApi> } = {}) { + return { + internalApi: getLensInternalApiMock(internalApiOverrides), + api: getLensApiMock(apiOverrides), + onUnmount: jest.fn(), + }; +} + +describe('Lens Embeddable component', () => { + it('should not render the visualization if any error arises', () => { + const props = getDefaultProps({ + internalApiOverrides: { + expressionParams$: new BehaviorSubject<GetValueType<LensInternalApi['expressionParams$']>>( + null + ), + }, + }); + + render(<LensEmbeddableComponent {...props} />); + expect(screen.queryByTestId('lens-embeddable')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx new file mode 100644 index 0000000000000..6d98b901d905f --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/renderer/lens_embeddable_component.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import React, { useEffect } from 'react'; +import { LensApi } from '../..'; +import { ExpressionWrapper } from '../expression_wrapper'; +import { LensInternalApi } from '../types'; +import { UserMessages } from '../user_messages/container'; +import { useMessages, useDispatcher } from './hooks'; +import { getViewMode } from '../helper'; +import { addLog } from '../logger'; + +export function LensEmbeddableComponent({ + internalApi, + api, + onUnmount, +}: { + internalApi: LensInternalApi; + api: LensApi; + onUnmount: () => void; +}) { + const [ + // Pick up updated params from the observable + expressionParams, + // used for functional tests + renderCount, + // has the render completed? + hasRendered, + // these are blocking errors that can be shown in a badge + // without replacing the entire panel + blockingErrors, + // has view mode changed? + latestViewMode, + ] = useBatchedPublishingSubjects( + internalApi.expressionParams$, + internalApi.renderCount$, + internalApi.hasRenderCompleted$, + internalApi.validationMessages$, + api.viewMode + ); + const canEdit = Boolean(api.isEditingEnabled?.() && getViewMode(latestViewMode) === 'edit'); + + const [warningOrErrors, infoMessages] = useMessages(internalApi); + + // On unmount call all the cleanups + useEffect(() => { + addLog(`Mounting Lens Embeddable component: ${api.defaultPanelTitle?.getValue()}`); + return onUnmount; + }, [api, onUnmount]); + + // take care of dispatching the event from the DOM node + const rootRef = useDispatcher(hasRendered, api); + + // Publish the data attributes only if avaialble/visible + const title = api.hidePanelTitle?.getValue() + ? undefined + : { 'data-title': api.panelTitle?.getValue() ?? api.defaultPanelTitle?.getValue() }; + const description = api.panelDescription?.getValue() + ? { + 'data-description': + api.panelDescription?.getValue() ?? api.defaultPanelDescription?.getValue(), + } + : undefined; + + return ( + <div + style={{ width: '100%', height: '100%' }} + data-rendering-count={renderCount + 1} + data-render-complete={hasRendered} + {...title} + {...description} + data-shared-item + ref={rootRef} + > + {expressionParams == null || blockingErrors.length ? null : ( + <ExpressionWrapper {...expressionParams} /> + )} + <UserMessages + blockingErrors={blockingErrors} + warningOrErrors={warningOrErrors} + infoMessages={infoMessages} + canEdit={canEdit} + /> + </div> + ); +} diff --git a/x-pack/plugins/lens/public/react_embeddable/type_guards.ts b/x-pack/plugins/lens/public/react_embeddable/type_guards.ts new file mode 100644 index 0000000000000..95e8311a7a3c0 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/type_guards.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 { + apiIsOfType, + apiPublishesPanelTitle, + apiPublishesUnifiedSearch, +} from '@kbn/presentation-publishing'; +import { isObject } from 'lodash'; +import { + LensApiCallbacks, + LensApi, + LensComponentForwardedProps, + LensPublicCallbacks, +} from './types'; + +function apiHasLensCallbacks(api: unknown): api is LensApiCallbacks { + const fns = [ + 'getSavedVis', + 'getViewUnderlyingDataArgs', + 'isTextBasedLanguage', + 'getTextBasedLanguage', + ] as Array<keyof LensApiCallbacks>; + return fns.every((fn) => typeof (api as LensApiCallbacks)[fn] === 'function'); +} + +export const isLensApi = (api: unknown): api is LensApi => { + return Boolean( + api && + apiIsOfType(api, 'lens') && + 'canViewUnderlyingData$' in api && + apiHasLensCallbacks(api) && + apiPublishesPanelTitle(api) && + apiPublishesUnifiedSearch(api) + ); +}; + +export function apiHasLensComponentCallbacks(api: unknown): api is LensPublicCallbacks { + return ( + isObject(api) && + ['onFilter', 'onBrushEnd', 'onLoad', 'onTableRowClick', 'onBeforeBadgesRender'].some((fn) => + Object.hasOwn(api, fn) + ) + ); +} + +export function apiHasLensComponentProps(api: unknown): api is LensComponentForwardedProps { + return ( + isObject(api) && + ['style', 'className', 'noPadding', 'viewMode', 'abortController'].some((prop) => + Object.hasOwn(api, prop) + ) + ); +} + +export function apiHasAbortController(api: unknown): api is { abortController: AbortController } { + return isObject(api) && Object.hasOwn(api, 'abortController'); +} + +export function apiHasLastReloadRequestTime( + api: unknown +): api is { lastReloadRequestTime: number } { + return isObject(api) && Object.hasOwn(api, 'lastReloadRequestTime'); +} + +export function apiPublishesInlineEditingCapabilities( + api: unknown +): api is { canEditInline: boolean } { + return isObject(api) && Object.hasOwn(api, 'canEditInline'); +} diff --git a/x-pack/plugins/lens/public/react_embeddable/types.ts b/x-pack/plugins/lens/public/react_embeddable/types.ts new file mode 100644 index 0000000000000..03a9801507d1c --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/types.ts @@ -0,0 +1,494 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import type { + AggregateQuery, + ExecutionContextSearch, + Filter, + Query, + TimeRange, +} from '@kbn/es-query'; +import type { Adapters, InspectorOptions } from '@kbn/inspector-plugin/public'; +import type { + HasEditCapabilities, + HasInPlaceLibraryTransforms, + HasLibraryTransforms, + HasSupportedTriggers, + PublishesBlockingError, + PublishesDataLoading, + PublishesDataViews, + PublishesDisabledActionIds, + PublishesSavedObjectId, + PublishesUnifiedSearch, + PublishesViewMode, + PublishesWritablePanelDescription, + PublishesWritablePanelTitle, + PublishingSubject, + SerializedTitles, + ViewMode, +} from '@kbn/presentation-publishing'; +import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin'; +import type { + BrushTriggerEvent, + ClickTriggerEvent, + MultiClickTriggerEvent, +} from '@kbn/charts-plugin/public'; +import type { PaletteOutput } from '@kbn/coloring'; +import type { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/common'; +import type { + Capabilities, + CoreStart, + HttpSetup, + IUiSettingsClient, + KibanaExecutionContext, + OverlayRef, + SavedObjectReference, + ThemeServiceStart, +} from '@kbn/core/public'; +import type { TimefilterContract, FilterManager } from '@kbn/data-plugin/public'; +import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { + ExpressionRendererEvent, + ReactExpressionRendererProps, + ReactExpressionRendererType, +} from '@kbn/expressions-plugin/public'; +import type { RecursiveReadonly } from '@kbn/utility-types'; +import type { AllowedChartOverrides, AllowedSettingsOverrides } from '@kbn/charts-plugin/common'; +import type { AllowedGaugeOverrides } from '@kbn/expression-gauge-plugin/common'; +import type { AllowedPartitionOverrides } from '@kbn/expression-partition-vis-plugin/common'; +import type { AllowedXYOverrides } from '@kbn/expression-xy-plugin/common'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import type { LegacyMetricState } from '../../common'; +import type { LensDocument } from '../persistence'; +import type { LensInspector } from '../lens_inspector_service'; +import type { LensAttributesService } from '../lens_attribute_service'; +import type { + DatatableVisualizationState, + DocumentToExpressionReturnType, + HeatmapVisualizationState, + XYState, +} from '../async_services'; +import type { + DatasourceMap, + IndexPatternMap, + IndexPatternRef, + LensTableRowContextMenuEvent, + SharingSavedObjectProps, + Simplify, + UserMessage, + VisualizationMap, +} from '../types'; +import type { LensPluginStartDependencies } from '../plugin'; +import type { TableInspectorAdapter } from '../editor_frame_service/types'; +import type { PieVisualizationState } from '../../common/types'; +import type { FormBasedPersistedState } from '..'; +import type { TextBasedPersistedState } from '../datasources/text_based/types'; +import type { GaugeVisualizationState } from '../visualizations/gauge/constants'; +import type { MetricVisualizationState } from '../visualizations/metric/types'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface LensApiProps {} + +export type LensSavedObjectAttributes = Omit<LensDocument, 'savedObjectId' | 'type'>; + +export interface VisualizationContext { + doc: LensDocument | undefined; + mergedSearchContext: ExecutionContextSearch; + indexPatterns: IndexPatternMap; + indexPatternRefs: IndexPatternRef[]; + activeVisualizationState: unknown; + activeDatasourceState: unknown; + activeData?: TableInspectorAdapter; +} + +export interface VisualizationContextHelper { + getVisualizationContext: () => VisualizationContext; + updateVisualizationContext: (newContext: Partial<VisualizationContext>) => void; +} + +export interface ViewUnderlyingDataArgs { + dataViewSpec: DataViewSpec; + timeRange: TimeRange; + filters: Filter[]; + query: Query | AggregateQuery | undefined; + columns: string[]; +} + +export type LensEmbeddableStartServices = Simplify< + LensPluginStartDependencies & { + timefilter: TimefilterContract; + coreHttp: HttpSetup; + coreStart: CoreStart; + capabilities: RecursiveReadonly<Capabilities>; + expressionRenderer: ReactExpressionRendererType; + documentToExpression: (doc: LensDocument) => Promise<DocumentToExpressionReturnType>; + injectFilterReferences: FilterManager['inject']; + visualizationMap: VisualizationMap; + datasourceMap: DatasourceMap; + theme: ThemeServiceStart; + uiSettings: IUiSettingsClient; + attributeService: LensAttributesService; + } +>; + +export interface PreventableEvent { + preventDefault(): void; +} + +interface LensByValue { + // by-value + attributes?: Simplify<LensSavedObjectAttributes>; +} + +export interface LensOverrides { + /** + * Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline. + * Each visualization type offers various type of overrides, per component (i.e. 'setting', 'axisX', 'partition', etc...) + * + * While it is not possible to pass function/callback/handlers to the renderer, it is possible to overwrite + * the current behaviour by passing the "ignore" string to the override prop (i.e. onBrushEnd: "ignore" to stop brushing) + */ + overrides?: + | AllowedChartOverrides + | AllowedSettingsOverrides + | AllowedXYOverrides + | AllowedPartitionOverrides + | AllowedGaugeOverrides; +} + +/** + * Lens embeddable props broken down by type + */ + +export interface LensByReference { + // by-reference + savedObjectId?: string; +} + +interface ContentManagementProps { + sharingSavedObjectProps?: SharingSavedObjectProps; + managed?: boolean; +} + +export type LensPropsVariants = (LensByValue & LensByReference) & { + references?: SavedObjectReference[]; +}; + +export interface ViewInDiscoverCallbacks extends LensApiProps { + canViewUnderlyingData$: PublishingSubject<boolean>; + loadViewUnderlyingData: () => void; + getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs | undefined; +} + +export interface IntegrationCallbacks extends LensApiProps { + isTextBasedLanguage: () => boolean | undefined; + getTextBasedLanguage: () => string | undefined; + getSavedVis: () => Readonly<LensSavedObjectAttributes | undefined>; + getFullAttributes: () => LensDocument | undefined; + updateAttributes: (newAttributes: LensRuntimeState['attributes']) => void; + updateSavedObjectId: (newSavedObjectId: LensRuntimeState['savedObjectId']) => void; + updateOverrides: (newOverrides: LensOverrides['overrides']) => void; + getTriggerCompatibleActions: (triggerId: string, context: object) => Promise<Action[]>; +} + +/** + * Public Callbacks are function who are exposed thru the Lens custom renderer component, + * so not directly exposed in the Lens API, rather passed down as parentApi to the Lens Embeddable + */ +export interface LensPublicCallbacks extends LensApiProps { + onBrushEnd?: (data: Simplify<BrushTriggerEvent['data'] & PreventableEvent>) => void; + onLoad?: ( + isLoading: boolean, + adapters?: Partial<DefaultInspectorAdapters>, + dataLoading$?: PublishingSubject<boolean | undefined> + ) => void; + onFilter?: ( + data: Simplify<(ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) & PreventableEvent> + ) => void; + onTableRowClick?: ( + data: Simplify<LensTableRowContextMenuEvent['data'] & PreventableEvent> + ) => void; + /** + * Let the consumer overwrite embeddable user messages + */ + onBeforeBadgesRender?: (userMessages: UserMessage[]) => UserMessage[]; +} + +/** + * API callbacks are function who are used by direct Embeddable consumers (i.e. Dashboard or our own Lens custom renderer) + */ +export type LensApiCallbacks = Simplify<ViewInDiscoverCallbacks & IntegrationCallbacks>; + +export interface LensUnifiedSearchContext { + filters?: Filter[]; + query?: Query | AggregateQuery; + timeRange?: TimeRange; + timeslice?: [number, number]; + searchSessionId?: string; + lastReloadRequestTime?: number; +} + +export interface LensPanelProps { + id?: string; + renderMode?: ViewMode; + disableTriggers?: boolean; + syncColors?: boolean; + syncTooltips?: boolean; + syncCursor?: boolean; + palette?: PaletteOutput; +} + +/** + * This set of props are exposes by the Lens component too + */ +export interface LensSharedProps { + executionContext?: KibanaExecutionContext; + style?: React.CSSProperties; + className?: string; + noPadding?: boolean; + viewMode?: ViewMode; +} + +interface LensRequestHandlersProps { + /** + * Custom abort controller to be used for the ES client + */ + abortController?: AbortController; +} + +/** + * Compose together all the props and make them inspectable via Simplify + * + * The LensSerializedState is the state stored for a dashboard panel + * that contains: + * * Lens document state + * * Panel settings + * * other props from the embeddable + */ +export type LensSerializedState = Simplify< + LensPropsVariants & + LensOverrides & + LensUnifiedSearchContext & + LensPanelProps & + SerializedTitles & + LensSharedProps & + Partial<DynamicActionsSerializedState> & { isNewPanel?: boolean } +>; + +/** + * Custom props exposed on the Lens exported component + */ +export type LensComponentProps = Simplify< + LensRequestHandlersProps & + LensSharedProps & { + /** + * When enabled the Lens component will render as a dashboard panel + */ + withDefaultActions?: boolean; + /** + * Allow custom actions to be rendered in the panel + */ + extraActions?: Action[]; + /** + * Disable specific actions for the embeddable + */ + disabledActions?: string[]; + /** + * Toggles the inspector + */ + showInspector?: boolean; + /** + * Toggle inline editing feature + */ + canEditInline?: boolean; + } +>; + +/** + * This is the subset of props that from the LensComponent will be forwarded to the Lens embeddable + */ +export type LensComponentForwardedProps = Pick< + LensComponentProps, + 'style' | 'className' | 'noPadding' | 'abortController' | 'executionContext' | 'viewMode' +>; + +/** + * Carefully chosen props to expose on the Lens renderer component used by + * other plugins + */ + +type ComponentProps = LensComponentProps & LensPublicCallbacks; +type ComponentSerializedProps = TypedLensSerializedState; + +type LensRendererPrivateProps = ComponentSerializedProps & ComponentProps; +export type LensRendererProps = Simplify<LensRendererPrivateProps>; + +/** + * The LensRuntimeState is the state stored for a dashboard panel + * that contains: + * * Lens document state + * * Panel settings + * * other props from the embeddable + */ +export type LensRuntimeState = Simplify< + Omit<ComponentSerializedProps, 'attributes' | 'references'> & { + attributes: NonNullable<LensSerializedState['attributes']>; + } & Pick<LensComponentForwardedProps, 'viewMode' | 'abortController' | 'executionContext'> & + ContentManagementProps +>; + +export interface LensInspectorAdapters { + getInspectorAdapters: () => Adapters; + inspect: (options?: InspectorOptions) => OverlayRef; + closeInspector: () => Promise<void>; + // expose a handler for the inspector adapters + // to be able to subscribe to changes + // a typical use case is the inline editing, where the editor + // needs to be updated on data changes + adapters$: PublishingSubject<Adapters>; +} + +export type LensApi = Simplify< + DefaultEmbeddableApi<LensSerializedState, LensRuntimeState> & + // This is used by actions to operate the edit action + HasEditCapabilities & + // for blocking errors leverage the embeddable panel UI + PublishesBlockingError & + // This is used by dashboard/container to show filters/queries on the panel + PublishesUnifiedSearch & + // Let the container know the loading state + PublishesDataLoading & + // Let the container know the used data views + PublishesDataViews & + // Let the container operate on panel title/description + PublishesWritablePanelTitle & + PublishesWritablePanelDescription & + // This embeddable can narrow down specific triggers usage + HasSupportedTriggers & + PublishesDisabledActionIds & + // Offers methods to operate from/on the linked saved object + HasInPlaceLibraryTransforms & + HasLibraryTransforms<LensRuntimeState> & + // Let the container know the view mode + PublishesViewMode & + // Let the container know the saved object id + PublishesSavedObjectId & + // Lens specific API methods: + // Let the container know when the data has been loaded/updated + LensInspectorAdapters & + LensRequestHandlersProps & + LensApiCallbacks +>; + +// This is an API only used internally to the embeddable but not exported elsewhere +// there's some overlapping between this and the LensApi but they are shared references +export type LensInternalApi = Simplify< + Pick<IntegrationCallbacks, 'updateAttributes' | 'updateOverrides'> & + PublishesDataViews & { + attributes$: PublishingSubject<LensRuntimeState['attributes']>; + overrides$: PublishingSubject<LensOverrides['overrides']>; + disableTriggers$: PublishingSubject<LensPanelProps['disableTriggers']>; + dataLoading$: PublishingSubject<boolean | undefined>; + hasRenderCompleted$: PublishingSubject<boolean>; + isNewlyCreated$: PublishingSubject<boolean>; + setAsCreated: () => void; + dispatchRenderStart: () => void; + dispatchRenderComplete: () => void; + dispatchError: () => void; + updateDataLoading: (newDataLoading: boolean | undefined) => void; + expressionParams$: PublishingSubject<ExpressionWrapperProps | null>; + updateExpressionParams: (newParams: ExpressionWrapperProps | null) => void; + expressionAbortController$: PublishingSubject<AbortController | undefined>; + updateAbortController: (newAbortController: AbortController | undefined) => void; + renderCount$: PublishingSubject<number>; + updateDataViews: (dataViews: DataView[] | undefined) => void; + messages$: PublishingSubject<UserMessage[]>; + updateMessages: (newMessages: UserMessage[]) => void; + validationMessages$: PublishingSubject<UserMessage[]>; + updateValidationMessages: (newMessages: UserMessage[]) => void; + resetAllMessages: () => void; + } +>; + +export interface ExpressionWrapperProps { + ExpressionRenderer: ReactExpressionRendererType; + expression: string | null; + variables?: Record<string, unknown>; + interactive?: boolean; + searchContext: ExecutionContextSearch; + searchSessionId?: string; + handleEvent: (event: ExpressionRendererEvent) => void; + onData$: ( + data: unknown, + inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined + ) => void; + onRender$: (count: number) => void; + renderMode?: RenderMode; + syncColors?: boolean; + syncTooltips?: boolean; + syncCursor?: boolean; + hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; + getCompatibleCellValueActions?: ReactExpressionRendererProps['getCompatibleCellValueActions']; + style?: React.CSSProperties; + className?: string; + addUserMessages: (messages: UserMessage[]) => void; + onRuntimeError: (error: Error) => void; + executionContext?: KibanaExecutionContext; + lensInspector: LensInspector; + noPadding?: boolean; + abortController?: AbortController; +} + +export type GetStateType = () => LensRuntimeState; + +/** + * Custom Lens component exported by the plugin + * For better DX of Lens component consumers, expose a typed version of the serialized state + */ + +/** Utility function to build typed version for each chart */ +type TypedLensAttributes<TVisType, TVisState> = Simplify< + Omit<LensDocument, 'savedObjectId' | 'type' | 'state' | 'visualizationType'> & { + visualizationType: TVisType; + state: Simplify< + Omit<LensDocument['state'], 'datasourceStates' | 'visualization'> & { + datasourceStates: { + formBased?: FormBasedPersistedState; + textBased?: TextBasedPersistedState; + }; + visualization: TVisState; + } + >; + } +>; + +/** + * Type-safe variant of by value embeddable input for Lens. + * This can be used to hardcode certain Lens chart configurations within another app. + */ +export type TypedLensSerializedState = Simplify< + Omit<LensSerializedState, 'attributes'> & { + attributes: + | TypedLensAttributes<'lnsXY', XYState> + | TypedLensAttributes<'lnsPie', PieVisualizationState> + | TypedLensAttributes<'lnsHeatmap', HeatmapVisualizationState> + | TypedLensAttributes<'lnsGauge', GaugeVisualizationState> + | TypedLensAttributes<'lnsDatatable', DatatableVisualizationState> + | TypedLensAttributes<'lnsLegacyMetric', LegacyMetricState> + | TypedLensAttributes<'lnsMetric', MetricVisualizationState> + | TypedLensAttributes<string, unknown>; + } +>; + +/** + * Backward compatibility types + */ +export type LensByValueInput = Omit<LensRendererPrivateProps, 'savedObjectId'>; +export type LensByReferenceInput = Omit<LensRendererPrivateProps, 'attributes'>; +export type TypedLensByValueInput = Omit<LensRendererProps, 'savedObjectId'>; +export type LensEmbeddableInput = LensByValueInput | LensByReferenceInput; +export type LensEmbeddableOutput = LensApi; diff --git a/x-pack/plugins/lens/public/react_embeddable/user_messages/api.ts b/x-pack/plugins/lens/public/react_embeddable/user_messages/api.ts new file mode 100644 index 0000000000000..90061cfb7c2fe --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/api.ts @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SpacesApi } from '@kbn/spaces-plugin/public'; +import { Adapters } from '@kbn/inspector-plugin/common'; +import { BehaviorSubject } from 'rxjs'; +import { + filterAndSortUserMessages, + getApplicationUserMessages, + handleMessageOverwriteFromConsumer, +} from '../../app_plugin/get_application_user_messages'; +import { getDatasourceLayers } from '../../state_management/utils'; +import { + UserMessagesGetter, + UserMessage, + FramePublicAPI, + SharingSavedObjectProps, +} from '../../types'; +import { + getActiveDatasourceIdFromDoc, + getActiveVisualizationIdFromDoc, + getInitialDataViewsObject, +} from '../../utils'; +import { + LensPublicCallbacks, + LensEmbeddableStartServices, + VisualizationContext, + VisualizationContextHelper, + LensApi, + LensInternalApi, +} from '../types'; +import { getLegacyURLConflictsMessage, hasLegacyURLConflict } from './checks'; +import { getSearchWarningMessages } from '../../utils'; +import { addLog } from '../logger'; + +function getUpdatedState( + getVisualizationContext: VisualizationContextHelper['getVisualizationContext'], + visualizationMap: LensEmbeddableStartServices['visualizationMap'], + datasourceMap: LensEmbeddableStartServices['datasourceMap'] +) { + const { + doc, + mergedSearchContext, + indexPatterns, + indexPatternRefs, + activeVisualizationState, + activeDatasourceState, + activeData, + } = getVisualizationContext(); + const activeVisualizationId = getActiveVisualizationIdFromDoc(doc); + const activeDatasourceId = getActiveDatasourceIdFromDoc(doc); + const activeDatasource = activeDatasourceId ? datasourceMap[activeDatasourceId] : null; + const activeVisualization = activeVisualizationId + ? visualizationMap[activeVisualizationId] + : undefined; + const dataViewObject = getInitialDataViewsObject(indexPatterns, indexPatternRefs); + return { + doc, + mergedSearchContext, + activeDatasource, + activeVisualization, + activeVisualizationId, + dataViewObject, + activeVisualizationState, + activeDatasourceState, + activeDatasourceId, + activeData, + }; +} + +function getWarningMessages( + { + activeDatasource, + activeDatasourceId, + activeDatasourceState, + }: ReturnType<typeof getUpdatedState>, + adapters: Adapters, + data: LensEmbeddableStartServices['data'] +) { + if (!activeDatasource || !activeDatasourceId || !adapters?.requests) { + return []; + } + + const requestWarnings = getSearchWarningMessages( + adapters.requests, + activeDatasource, + activeDatasourceState, + { + searchService: data.search, + } + ); + + return requestWarnings; +} + +export function buildUserMessagesHelpers( + api: LensApi, + internalApi: LensInternalApi, + getVisualizationContext: () => VisualizationContext, + { coreStart, data, visualizationMap, datasourceMap }: LensEmbeddableStartServices, + onBeforeBadgesRender: LensPublicCallbacks['onBeforeBadgesRender'], + spaces?: SpacesApi, + metaInfo?: SharingSavedObjectProps +): { + getUserMessages: UserMessagesGetter; + addUserMessages: (messages: UserMessage[]) => void; + updateWarnings: () => void; + updateMessages: (messages: UserMessage[]) => void; + resetMessages: () => void; + updateBlockingErrors: (blockingMessages: UserMessage[] | Error) => void; + updateValidationErrors: (messages: UserMessage[]) => void; +} { + let runtimeUserMessages: Record<string, UserMessage> = {}; + const addUserMessages = (messages: UserMessage[]) => { + if (messages.length) { + addLog(`addUserMessages: "${messages.map(({ uniqueId }) => uniqueId).join('", "')}"`); + } + for (const message of messages) { + runtimeUserMessages[message.uniqueId] = message; + } + }; + + const resetMessages = () => { + runtimeUserMessages = {}; + internalApi.resetAllMessages(); + }; + + const getUserMessages: UserMessagesGetter = (locationId, filters) => { + const { + doc, + activeVisualizationState, + activeVisualization, + activeVisualizationId, + activeDatasource, + activeDatasourceState, + activeDatasourceId, + dataViewObject, + mergedSearchContext, + activeData, + } = getUpdatedState(getVisualizationContext, visualizationMap, datasourceMap); + const userMessages: UserMessage[] = []; + + userMessages.push( + ...getApplicationUserMessages({ + visualizationType: doc?.visualizationType, + visualizationState: { + state: activeVisualizationState, + activeId: activeVisualizationId, + }, + visualization: activeVisualization, + activeDatasource, + activeDatasourceState: { + isLoading: !activeDatasourceState, + state: activeDatasourceState, + }, + dataViews: dataViewObject, + core: coreStart, + }) + ); + + if (!doc || !activeDatasourceState || !activeVisualizationState) { + return userMessages; + } + + const framePublicAPI: FramePublicAPI = { + dataViews: dataViewObject, + datasourceLayers: getDatasourceLayers( + { + [activeDatasourceId!]: { + isLoading: !activeDatasourceState, + state: activeDatasourceState, + }, + }, + datasourceMap, + dataViewObject.indexPatterns + ), + query: doc.state.query, + filters: mergedSearchContext.filters ?? [], + dateRange: { + fromDate: mergedSearchContext.timeRange?.from ?? '', + toDate: mergedSearchContext.timeRange?.to ?? '', + }, + absDateRange: { + fromDate: mergedSearchContext.timeRange?.from ?? '', + toDate: mergedSearchContext.timeRange?.to ?? '', + }, + activeData, + }; + + if (hasLegacyURLConflict(metaInfo, spaces)) { + userMessages.push(getLegacyURLConflictsMessage(metaInfo!, spaces!)); + } + + userMessages.push( + ...(activeDatasource?.getUserMessages(activeDatasourceState, { + setState: () => {}, + frame: framePublicAPI, + visualizationInfo: activeVisualization?.getVisualizationInfo?.( + activeVisualizationState, + framePublicAPI + ), + }) ?? []), + ...(activeVisualization?.getUserMessages?.(activeVisualizationState, { + frame: framePublicAPI, + }) ?? []) + ); + + return handleMessageOverwriteFromConsumer( + filterAndSortUserMessages( + userMessages.concat(Object.values(runtimeUserMessages)), + locationId, + filters ?? {} + ), + onBeforeBadgesRender + ); + }; + + return { + addUserMessages, + resetMessages, + getUserMessages, + /** + * Here pass all the messages that comes directly from the Lens validation/info system + * who includes: + * * configuration errors (i.e. missing fields) + * * warning messages (badge related) + * * info messages (badge related) + */ + updateMessages: (messages: UserMessage[]) => { + // update the messages only if something changed + const existingMessages = new Set( + internalApi.messages$.getValue().map(({ uniqueId }) => uniqueId) + ); + if ( + existingMessages.size !== messages.length || + messages.some(({ uniqueId }) => !existingMessages.has(uniqueId)) + ) { + internalApi.updateMessages(messages); + } + }, + updateValidationErrors: (messages: UserMessage[]) => { + addLog( + `Validation error: ${ + messages.length ? messages.map(({ uniqueId }) => uniqueId).join(', ') : 'No errors' + }` + ); + internalApi.updateValidationMessages(messages); + }, + /** + * This type of errors are those who need to be rendered in the embeddable native error panel + * like runtime errors. + */ + updateBlockingErrors: (blockingMessages: UserMessage[] | Error) => { + const error = + blockingMessages instanceof Error + ? blockingMessages + : blockingMessages.length + ? new Error( + typeof blockingMessages[0].longMessage === 'string' && blockingMessages[0].longMessage + ? blockingMessages[0].longMessage + : blockingMessages[0].shortMessage + ) + : undefined; + + if (error) { + addLog(`Blocking error: ${error?.message}`); + } + + if (error?.message !== api.blockingError.getValue()?.message) { + const finalError = error?.message === '' ? undefined : error; + (api.blockingError as BehaviorSubject<Error | undefined>).next(finalError); + } + }, + updateWarnings: () => { + addUserMessages( + getWarningMessages( + getUpdatedState(getVisualizationContext, visualizationMap, datasourceMap), + api.adapters$.getValue(), + data + ) + ); + }, + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/user_messages/checks.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/checks.tsx new file mode 100644 index 0000000000000..50250b31fdc7c --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/checks.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { SpacesApi } from '@kbn/spaces-plugin/public'; +import React from 'react'; +import { DOC_TYPE } from '../../../common/constants'; +import type { + IndexPatternMap, + IndexPatternRef, + SharingSavedObjectProps, + UserMessage, +} from '../../types'; +import type { LensApi } from '../types'; +import type { MergedSearchContext } from '../expressions/merged_search_context'; +import { MISSING_TIME_RANGE_ON_EMBEDDABLE, URL_CONFLICT } from '../../user_messages_ids'; + +export function hasLegacyURLConflict(metaInfo?: SharingSavedObjectProps, spaces?: SpacesApi) { + return metaInfo?.outcome === 'conflict' && spaces?.ui?.components?.getEmbeddableLegacyUrlConflict; +} + +export function getLegacyURLConflictsMessage( + metaInfo: SharingSavedObjectProps, + spaces: SpacesApi +): UserMessage { + const LegacyURLConfig = spaces.ui.components.getEmbeddableLegacyUrlConflict; + return { + uniqueId: URL_CONFLICT, + severity: 'error', + displayLocations: [{ id: 'visualization' }], + shortMessage: i18n.translate('xpack.lens.legacyURLConflict.shortMessage', { + defaultMessage: `You've encountered a URL conflict`, + }), + longMessage: <LegacyURLConfig targetType={DOC_TYPE} sourceId={metaInfo.sourceId!} />, + fixableInEditor: false, + }; +} + +export function isSearchContextIncompatibleWithDataViews( + api: LensApi, + context: { type?: string; id?: string } | undefined, + searchContext: MergedSearchContext, + indexPatternRefs: IndexPatternRef[], + indexPatterns: IndexPatternMap +) { + return ( + !api.isTextBasedLanguage() && + searchContext.timeRange == null && + indexPatternRefs.some(({ id }) => { + const indexPattern = indexPatterns[id]; + return indexPattern?.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName); + }) + ); +} + +export function getSearchContextIncompatibleMessage(): UserMessage { + return { + uniqueId: MISSING_TIME_RANGE_ON_EMBEDDABLE, + severity: 'error', + fixableInEditor: false, + displayLocations: [{ id: 'visualization' }], + shortMessage: i18n.translate('xpack.lens.missingTimeRangeParam.shortMessage', { + defaultMessage: `Missing timeRange property`, + }), + longMessage: i18n.translate('xpack.lens.missingTimeRangeParam.longMessage', { + defaultMessage: `The timeRange property is required for the given configuration`, + }), + }; +} diff --git a/x-pack/plugins/lens/public/react_embeddable/user_messages/container.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/container.tsx new file mode 100644 index 0000000000000..451de837e96e7 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/container.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 { css } from '@emotion/react'; +import React from 'react'; +import type { UserMessage } from '../../types'; +import { VisualizationErrorPanel } from './error_panel'; +import { EmbeddableFeatureBadge } from './info_badges'; +import { MessagesPopover } from './message_popover'; + +export function UserMessages({ + blockingErrors, + warningOrErrors, + infoMessages, + canEdit, +}: { + canEdit: boolean; + blockingErrors: UserMessage[]; + warningOrErrors: UserMessage[]; + infoMessages: UserMessage[]; +}) { + if (!blockingErrors.length && !warningOrErrors.length && !infoMessages.length) { + return null; + } + return ( + <> + <VisualizationErrorPanel errors={blockingErrors} canEdit={canEdit} /> + <div + css={css({ + position: 'absolute', + zIndex: 2, + left: 0, + bottom: 0, + })} + > + <MessagesPopover messages={warningOrErrors} /> + <EmbeddableFeatureBadge messages={infoMessages} /> + </div> + </> + ); +} diff --git a/x-pack/plugins/lens/public/react_embeddable/user_messages/error_panel.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/error_panel.tsx new file mode 100644 index 0000000000000..ee050382914c8 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/error_panel.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { UserMessage } from '../../types'; +import { getLongMessage } from '../../user_messages_utils'; + +export function VisualizationErrorPanel({ + errors, + canEdit, +}: { + errors: UserMessage[]; + canEdit: boolean; +}) { + if (!errors.length) { + return null; + } + const showMore = errors.length > 1; + const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor); + return ( + <div className="lnsEmbeddedError"> + <EuiEmptyPrompt + iconType="warning" + iconColor="danger" + data-test-subj="embeddable-lens-failure" + body={ + <> + {errors.length ? ( + <> + <p>{getLongMessage(errors[0]) || errors[0].shortMessage}</p> + {showMore && !canFixInLens ? ( + <p> + <FormattedMessage + id="xpack.lens.moreErrors" + defaultMessage="Edit in Lens editor to see more errors" + /> + </p> + ) : null} + {canFixInLens ? ( + <p> + <FormattedMessage + id="xpack.lens.fixErrors" + defaultMessage="Edit in Lens editor to fix the error" + /> + </p> + ) : null} + </> + ) : ( + <p> + <FormattedMessage + id="xpack.lens.failure" + defaultMessage="Visualization couldn't be displayed" + /> + </p> + )} + </> + } + /> + </div> + ); +} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.scss similarity index 62% rename from x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss rename to x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.scss index 55407855b49f6..7435808095a19 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.scss @@ -1,4 +1,4 @@ -.lnsEmbeddablePanelFeatureList { +.lnsPanelFeatureList { max-height: $euiSize * 20; @include euiYScroll; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.test.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.test.tsx similarity index 97% rename from x-pack/plugins/lens/public/embeddable/embeddable_info_badges.test.tsx rename to x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.test.tsx index b70b102a78484..ef3ee40e17d1e 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.test.tsx +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; -import { EmbeddableFeatureBadge } from './embeddable_info_badges'; -import { UserMessage } from '../types'; +import { EmbeddableFeatureBadge } from './info_badges'; +import { UserMessage } from '../../types'; describe('EmbeddableFeatureBadge', () => { async function renderPopup(messages: UserMessage[], count: number = messages.length) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.tsx similarity index 89% rename from x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx rename to x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.tsx index 18cff3f2ac90a..5b120625b662e 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/info_badges.tsx @@ -18,9 +18,9 @@ import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { useState } from 'react'; -import type { UserMessage } from '../types'; -import './embeddable_info_badges.scss'; -import { getLongMessage } from '../user_messages_utils'; +import type { UserMessage } from '../../types'; +import './info_badges.scss'; +import { getLongMessage } from '../../user_messages_utils'; export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }) => { const { euiTheme } = useEuiTheme(); @@ -31,7 +31,7 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } if (!messages.length) { return null; } - const iconTitle = i18n.translate('xpack.lens.embeddable.featureBadge.iconDescription', { + const iconTitle = i18n.translate('xpack.lens.featureBadge.iconDescription', { defaultMessage: `{count} visualization {count, plural, one {modifier} other {modifiers}}`, values: { count: messages.length, @@ -51,7 +51,7 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } <EuiToolTip content={iconTitle}> <EuiButtonEmpty data-test-subj="lns-feature-badges-trigger" - className="lnsEmbeddablePanelFeatureList_button" + className="lnsPanelFeatureList_button" color={'text'} onClick={onButtonClick} title={iconTitle} @@ -64,6 +64,9 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } .euiButtonEmpty__content { gap: ${euiTheme.size.xs}; } + &:hover { + color: ${euiTheme.colors.text}; + } `} iconType="wrench" > @@ -98,7 +101,7 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } <EuiTitle size="xxs" css={css`color=${euiTheme.colors.title}`}> <h3>{shortMessage}</h3> </EuiTitle> - <ul className="lnsEmbeddablePanelFeatureList"> + <ul className="lnsPanelFeatureList"> {messageGroup.map((message, i) => ( <Fragment key={`${uniqueId}-${i}`}>{getLongMessage(message)}</Fragment> ))} diff --git a/x-pack/plugins/lens/public/react_embeddable/user_messages/message_popover.tsx b/x-pack/plugins/lens/public/react_embeddable/user_messages/message_popover.tsx new file mode 100644 index 0000000000000..a6359bd683d13 --- /dev/null +++ b/x-pack/plugins/lens/public/react_embeddable/user_messages/message_popover.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 { useEuiTheme, useEuiFontSize } from '@elastic/eui'; +import { css } from '@emotion/react'; + +import React from 'react'; +import { MessageList } from '../../editor_frame_service/editor_frame/workspace_panel/message_list'; +import { UserMessage } from '../../types'; + +export const MessagesPopover = ({ messages }: { messages: UserMessage[] }) => { + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + + if (!messages.length) { + return null; + } + + return ( + <MessageList + messages={messages} + customButtonStyles={css` + block-size: ${euiTheme.size.l}; + font-size: ${xsFontSize}; + padding: 0 ${euiTheme.size.xs}; + & > * { + gap: ${euiTheme.size.xs}; + } + `} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap index a1ae0da676803..8af3d61bf668d 100644 --- a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap +++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap @@ -28,7 +28,6 @@ Object { "persistedDoc": Object { "exactMatchDoc": Object { "attributes": Object { - "expression": "definitely a valid expression", "references": Array [ Object { "id": "1", @@ -39,7 +38,10 @@ Object { "savedObjectId": "1234", "state": Object { "datasourceStates": Object { - "testDatasource": "datasource", + "testDatasource": Object { + "isLoading": false, + "state": Object {}, + }, }, "filters": Array [ Object { @@ -53,7 +55,10 @@ Object { }, }, ], - "query": "kuery", + "query": Object { + "language": "kuery", + "query": "test", + }, "visualization": Object {}, }, "title": "An extremely cool default document!", diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts index 0858d9d8af783..b0011c3c822ed 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts @@ -12,6 +12,7 @@ import { loadInitial as loadInitialAction } from '..'; import { loadInitial } from './load_initial'; import { readFromStorage } from '../../settings_storage'; import { AUTO_APPLY_DISABLED_STORAGE_KEY } from '../../editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper'; +import { type InitialAppState } from '../lens_slice'; const autoApplyDisabled = () => { return readFromStorage(new Storage(localStorage), AUTO_APPLY_DISABLED_STORAGE_KEY) === 'true'; @@ -20,7 +21,7 @@ const autoApplyDisabled = () => { export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAPI) => { return (next: Dispatch) => (action: PayloadAction) => { if (loadInitialAction.match(action)) { - return loadInitial(store, storeDeps, action.payload, autoApplyDisabled()); + return loadInitial(store, storeDeps, action.payload as InitialAppState, autoApplyDisabled()); } next(action); }; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 606ede8cd2686..458285096f7e7 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -9,51 +9,55 @@ import { cloneDeep } from 'lodash'; import { MiddlewareAPI } from '@reduxjs/toolkit'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; -import { setState, initExisting, initEmpty, LensStoreDeps } from '..'; -import { disableAutoApply, getPreloadedState } from '../lens_slice'; +import { setState, initExisting, initEmpty, LensStoreDeps, LensAppState } from '..'; +import { type InitialAppState, disableAutoApply, getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; -import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId, getInitialDataViewsObject } from '../../utils'; import { initializeSources } from '../../editor_frame_service/editor_frame'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; -import { Document } from '../../persistence'; +import { LensDocument } from '../../persistence'; +import { LensSerializedState } from '../../react_embeddable/types'; -export const getPersisted = async ({ +interface PersistedDoc { + doc: LensDocument; + sharingSavedObjectProps: Omit<SharingSavedObjectProps, 'sourceId'>; + managed: boolean; +} + +/** + * This function returns a Saved object from a either a by reference or by value input + */ +export const getFromPreloaded = async ({ initialInput, lensServices, history, }: { - initialInput: LensEmbeddableInput; + initialInput: LensSerializedState; lensServices: Pick<LensAppServices, 'attributeService' | 'notifications' | 'spaces' | 'http'>; history?: History<unknown>; -}): Promise< - | { - doc: Document; - sharingSavedObjectProps: Omit<SharingSavedObjectProps, 'sourceId'>; - managed: boolean; - } - | undefined -> => { +}): Promise<PersistedDoc | undefined> => { const { notifications, spaces, attributeService } = lensServices; - let doc: Document; + let doc: LensDocument; try { - const result = await attributeService.unwrapAttributes(initialInput); - if (!result) { + const docFromSavedObject = await (initialInput.savedObjectId + ? attributeService.loadFromLibrary(initialInput.savedObjectId) + : undefined); + if (!docFromSavedObject) { return { + // @TODO: it would be nice to address this type checks once for all doc: { - ...initialInput, + ...initialInput.attributes, type: LENS_EMBEDDABLE_TYPE, - } as unknown as Document, + } as LensDocument, sharingSavedObjectProps: { outcome: 'exactMatch', }, managed: false, }; } - const { metaInfo, attributes } = result; - const sharingSavedObjectProps = metaInfo?.sharingSavedObjectProps; + const { sharingSavedObjectProps, attributes, managed } = docFromSavedObject; if (spaces && sharingSavedObjectProps?.outcome === 'aliasMatch' && history) { // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash const newObjectId = sharingSavedObjectProps.aliasTargetId!; // This is always defined if outcome === 'aliasMatch' @@ -80,7 +84,7 @@ export const getPersisted = async ({ aliasTargetId: sharingSavedObjectProps?.aliasTargetId, outcome: sharingSavedObjectProps?.outcome, }, - managed: Boolean(metaInfo?.managed), + managed: Boolean(managed), }; } catch (e) { notifications.toasts.addDanger( @@ -91,30 +95,242 @@ export const getPersisted = async ({ } }; -export function loadInitial( +interface LoaderSharedArgs { + visualizationMap: LensStoreDeps['visualizationMap']; + datasourceMap: LensStoreDeps['datasourceMap']; + initialContext: LensStoreDeps['initialContext']; + dataViews: LensStoreDeps['lensServices']['dataViews']; + storage: LensStoreDeps['lensServices']['storage']; + eventAnnotationService: LensStoreDeps['lensServices']['eventAnnotationService']; + defaultIndexPatternId: string; +} + +type PreloadedState = Omit< + LensAppState, + 'resolvedDateRange' | 'searchSessionId' | 'isLinkedToOriginatingApp' +>; + +async function loadFromLocatorState( + store: MiddlewareAPI, + initialState: NonNullable<LensStoreDeps['initialStateFromLocator']>, + loaderSharedArgs: LoaderSharedArgs, + { notifications, data }: LensStoreDeps['lensServices'], + emptyState: PreloadedState, + autoApplyDisabled: boolean +) { + const { lens } = store.getState(); + const locatorReferences = 'references' in initialState ? initialState.references : undefined; + + const { + datasourceStates, + visualizationState, + indexPatterns, + indexPatternRefs, + annotationGroups, + } = await initializeSources( + { + visualizationState: emptyState.visualization, + datasourceStates: emptyState.datasourceStates, + adHocDataViews: lens.persistedDoc?.state.adHocDataViews || initialState.dataViewSpecs, + references: locatorReferences, + ...loaderSharedArgs, + }, + { + isFullEditor: true, + } + ); + const currentSessionId = initialState?.searchSessionId || data.search.session.getSessionId(); + store.dispatch( + initExisting({ + isSaveable: true, + filters: initialState.filters || data.query.filterManager.getFilters(), + query: initialState.query || emptyState.query, + searchSessionId: currentSessionId, + activeDatasourceId: emptyState.activeDatasourceId, + visualization: { + activeId: emptyState.visualization.activeId, + state: visualizationState, + }, + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + datasourceStates: Object.entries(datasourceStates).reduce( + (state, [datasourceId, datasourceState]) => ({ + ...state, + [datasourceId]: { + ...datasourceState, + isLoading: false, + }, + }), + {} + ), + isLoading: false, + annotationGroups, + }) + ); + + if (autoApplyDisabled) { + store.dispatch(disableAutoApply()); + } +} + +async function loadFromEmptyState( + store: MiddlewareAPI, + emptyState: PreloadedState, + loaderSharedArgs: LoaderSharedArgs, + { data }: LensStoreDeps['lensServices'], + activeDatasourceId: string | undefined, + autoApplyDisabled: boolean +) { + const { lens } = store.getState(); + const { datasourceStates, indexPatterns, indexPatternRefs } = await initializeSources( + { + visualizationState: lens.visualization, + datasourceStates: lens.datasourceStates, + adHocDataViews: lens.persistedDoc?.state.adHocDataViews, + ...loaderSharedArgs, + }, + { + isFullEditor: true, + } + ); + + store.dispatch( + initEmpty({ + newState: { + ...emptyState, + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + searchSessionId: data.search.session.getSessionId() || data.search.session.start(), + ...(activeDatasourceId && { activeDatasourceId }), + datasourceStates: Object.entries(datasourceStates).reduce( + (state, [datasourceId, datasourceState]) => ({ + ...state, + [datasourceId]: { + ...datasourceState, + isLoading: false, + }, + }), + {} + ), + isLoading: false, + }, + initialContext: loaderSharedArgs.initialContext, + }) + ); + if (autoApplyDisabled) { + store.dispatch(disableAutoApply()); + } +} + +async function loadFromSavedObject( + store: MiddlewareAPI, + savedObjectId: string | undefined, + persisted: PersistedDoc, + loaderSharedArgs: LoaderSharedArgs, + { data, chrome }: LensStoreDeps['lensServices'], + autoApplyDisabled: boolean, + inlineEditing?: boolean +) { + const { doc, sharingSavedObjectProps, managed } = persisted; + if (savedObjectId) { + chrome.recentlyAccessed.add(getFullPath(savedObjectId), doc.title, savedObjectId); + } + + const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce( + (stateMap, [datasourceId, datasourceState]) => ({ + ...stateMap, + [datasourceId]: { + isLoading: true, + state: datasourceState, + }, + }), + {} + ); + + // when the embeddable is initialized from the dashboard we don't want to inject the filters + // as this will replace the parent application filters (such as a dashboard) + if (!inlineEditing) { + const filters = data.query.filterManager.inject(doc.state.filters, doc.references); + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(filters); + } + + const docVisualizationState = { + activeId: doc.visualizationType, + state: doc.state.visualization, + }; + const { + datasourceStates, + visualizationState, + indexPatterns, + indexPatternRefs, + annotationGroups, + } = await initializeSources( + { + visualizationState: docVisualizationState, + datasourceStates: docDatasourceStates, + references: [...doc.references, ...(doc.state.internalReferences || [])], + adHocDataViews: doc.state.adHocDataViews, + ...loaderSharedArgs, + }, + { isFullEditor: true } + ); + const currentSessionId = data.search.session.getSessionId(); + store.dispatch( + initExisting({ + isSaveable: true, + sharingSavedObjectProps, + filters: data.query.filterManager.getFilters(), + query: doc.state.query, + searchSessionId: + !savedObjectId && currentSessionId + ? currentSessionId + : !inlineEditing + ? data.search.session.start() + : undefined, + persistedDoc: doc, + activeDatasourceId: getInitialDatasourceId(loaderSharedArgs.datasourceMap, doc), + visualization: { + activeId: doc.visualizationType, + state: visualizationState, + }, + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + datasourceStates: Object.entries(datasourceStates).reduce( + (state, [datasourceId, datasourceState]) => ({ + ...state, + [datasourceId]: { + ...datasourceState, + isLoading: false, + }, + }), + {} + ), + isLoading: false, + annotationGroups, + managed, + }) + ); + + if (autoApplyDisabled) { + store.dispatch(disableAutoApply()); + } +} + +export async function loadInitial( store: MiddlewareAPI, storeDeps: LensStoreDeps, - { - redirectCallback, - initialInput, - history, - inlineEditing, - }: { - redirectCallback?: (savedObjectId?: string) => void; - initialInput?: LensEmbeddableInput; - history?: History<unknown>; - inlineEditing?: boolean; - }, + { redirectCallback, initialInput, history, inlineEditing }: InitialAppState, autoApplyDisabled: boolean ) { const { lensServices, datasourceMap, initialContext, initialStateFromLocator, visualizationMap } = storeDeps; const { resolvedDateRange, searchSessionId, isLinkedToOriginatingApp, ...emptyState } = getPreloadedState(storeDeps); - const { attributeService, notifications, data } = lensServices; + const { notifications, data } = lensServices; const { lens } = store.getState(); - const loaderSharedArgs = { + const loaderSharedArgs: LoaderSharedArgs = { + visualizationMap, + initialContext, + datasourceMap, dataViews: lensServices.dataViews, storage: lensServices.storage, eventAnnotationService: lensServices.eventAnnotationService, @@ -144,79 +360,27 @@ export function loadInitial( // URL Reporting is using the locator params but also passing the savedObjectId // so be sure to not go here as there's no full snapshot URL if (!initialInput) { - const locatorReferences = - 'references' in initialStateFromLocator ? initialStateFromLocator.references : undefined; - - return initializeSources( - { - datasourceMap, - visualizationMap, - visualizationState: emptyState.visualization, - datasourceStates: emptyState.datasourceStates, - initialContext, - adHocDataViews: - lens.persistedDoc?.state.adHocDataViews || initialStateFromLocator.dataViewSpecs, - references: locatorReferences, - ...loaderSharedArgs, - }, - { - isFullEditor: true, - } - ) - .then( - ({ - datasourceStates, - visualizationState, - indexPatterns, - indexPatternRefs, - annotationGroups, - }) => { - const currentSessionId = - initialStateFromLocator?.searchSessionId || data.search.session.getSessionId(); - store.dispatch( - initExisting({ - isSaveable: true, - filters: initialStateFromLocator.filters || data.query.filterManager.getFilters(), - query: initialStateFromLocator.query || emptyState.query, - searchSessionId: currentSessionId, - activeDatasourceId: emptyState.activeDatasourceId, - visualization: { - activeId: emptyState.visualization.activeId, - state: visualizationState, - }, - dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), - datasourceStates: Object.entries(datasourceStates).reduce( - (state, [datasourceId, datasourceState]) => ({ - ...state, - [datasourceId]: { - ...datasourceState, - isLoading: false, - }, - }), - {} - ), - isLoading: false, - annotationGroups, - }) - ); - - if (autoApplyDisabled) { - store.dispatch(disableAutoApply()); - } - } - ) - .catch((e: { message: string }) => { - notifications.toasts.addDanger({ - title: e.message, - }); + try { + return loadFromLocatorState( + store, + initialStateFromLocator, + loaderSharedArgs, + lensServices, + emptyState, + autoApplyDisabled + ); + } catch ({ message }) { + notifications.toasts.addDanger({ + title: message, }); + return; + } } } if ( !initialInput || - (attributeService.inputIsRefType(initialInput) && - initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) + (initialInput.savedObjectId && initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { const newFilters = initialContext && 'searchFilters' in initialContext && initialContext.searchFilters @@ -226,179 +390,57 @@ export function loadInitial( if (newFilters) { data.query.filterManager.setAppFilters(newFilters); } - - return initializeSources( - { - datasourceMap, - visualizationMap, - visualizationState: lens.visualization, - datasourceStates: lens.datasourceStates, - initialContext, - adHocDataViews: lens.persistedDoc?.state.adHocDataViews, - ...loaderSharedArgs, - }, - { - isFullEditor: true, - } - ) - .then(({ datasourceStates, indexPatterns, indexPatternRefs }) => { - store.dispatch( - initEmpty({ - newState: { - ...emptyState, - dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), - searchSessionId: data.search.session.getSessionId() || data.search.session.start(), - ...(activeDatasourceId && { activeDatasourceId }), - datasourceStates: Object.entries(datasourceStates).reduce( - (state, [datasourceId, datasourceState]) => ({ - ...state, - [datasourceId]: { - ...datasourceState, - isLoading: false, - }, - }), - {} - ), - isLoading: false, - }, - initialContext, - }) - ); - if (autoApplyDisabled) { - store.dispatch(disableAutoApply()); - } - }) - .catch((e: { message: string }) => { - notifications.toasts.addDanger({ - title: e.message, - }); - redirectCallback?.(); + try { + return loadFromEmptyState( + store, + emptyState, + loaderSharedArgs, + lensServices, + activeDatasourceId, + autoApplyDisabled + ); + } catch ({ message }) { + notifications.toasts.addDanger({ + title: message, }); + return redirectCallback?.(); + } } - return getPersisted({ initialInput, lensServices, history }) - .then( - (persisted) => { - if (persisted) { - const { doc, sharingSavedObjectProps, managed } = persisted; - if (attributeService.inputIsRefType(initialInput)) { - lensServices.chrome.recentlyAccessed.add( - getFullPath(initialInput.savedObjectId), - doc.title, - initialInput.savedObjectId - ); - } - - const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce( - (stateMap, [datasourceId, datasourceState]) => ({ - ...stateMap, - [datasourceId]: { - isLoading: true, - state: datasourceState, - }, - }), - {} - ); - - // when the embeddable is initialized from the dashboard we don't want to inject the filters - // as this will replace the parent application filters (such as a dashboard) - if (!Boolean(inlineEditing)) { - const filters = data.query.filterManager.inject(doc.state.filters, doc.references); - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters(filters); - } - - const docVisualizationState = { - activeId: doc.visualizationType, - state: doc.state.visualization, - }; - return initializeSources( - { - datasourceMap, - visualizationMap, - visualizationState: docVisualizationState, - datasourceStates: docDatasourceStates, - references: [...doc.references, ...(doc.state.internalReferences || [])], - initialContext, - dataViews: lensServices.dataViews, - eventAnnotationService: lensServices.eventAnnotationService, - storage: lensServices.storage, - adHocDataViews: doc.state.adHocDataViews, - defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'), - }, - { isFullEditor: true } - ) - .then( - ({ - datasourceStates, - visualizationState, - indexPatterns, - indexPatternRefs, - annotationGroups, - }) => { - const currentSessionId = data.search.session.getSessionId(); - store.dispatch( - initExisting({ - isSaveable: true, - sharingSavedObjectProps, - filters: data.query.filterManager.getFilters(), - query: doc.state.query, - searchSessionId: - !(initialInput as LensByReferenceInput)?.savedObjectId && currentSessionId - ? currentSessionId - : !inlineEditing - ? data.search.session.start() - : undefined, - persistedDoc: doc, - activeDatasourceId: getInitialDatasourceId(datasourceMap, doc), - visualization: { - activeId: doc.visualizationType, - state: visualizationState, - }, - dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), - datasourceStates: Object.entries(datasourceStates).reduce( - (state, [datasourceId, datasourceState]) => ({ - ...state, - [datasourceId]: { - ...datasourceState, - isLoading: false, - }, - }), - {} - ), - isLoading: false, - annotationGroups, - managed, - }) - ); - - if (autoApplyDisabled) { - store.dispatch(disableAutoApply()); - } - } - ) - .catch((e: { message: string }) => - notifications.toasts.addDanger({ - title: e.message, - }) - ); - } else { - redirectCallback?.(); - } - }, - () => { - store.dispatch( - setState({ - isLoading: false, - }) + try { + const persisted = await getFromPreloaded({ initialInput, lensServices, history }); + if (persisted) { + try { + return loadFromSavedObject( + store, + initialInput.savedObjectId, + persisted, + loaderSharedArgs, + lensServices, + autoApplyDisabled, + inlineEditing ); - redirectCallback?.(); + } catch ({ message }) { + notifications.toasts.addDanger({ + title: message, + }); } - ) - .catch((e: { message: string }) => { + } else { + return redirectCallback?.(); + } + } catch (e) { + try { + store.dispatch( + setState({ + isLoading: false, + }) + ); + redirectCallback?.(); + } catch ({ message }) { notifications.toasts.addDanger({ - title: e.message, + title: message, }); redirectCallback?.(); - }); + } + } } diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 20c727734aa93..b2a9beb0fb0af 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -14,7 +14,6 @@ import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { EventAnnotationGroupConfig } from '@kbn/event-annotation-common'; import { DragDropIdentifier, DropType } from '@kbn/dom-drag-drop'; import { SeriesType } from '@kbn/visualizations-plugin/common'; -import { LensEmbeddableInput } from '..'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import type { VisualizeEditorContext, @@ -34,6 +33,7 @@ import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../t import { selectDataViews, selectFramePublicAPI } from './selectors'; import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; import type { LensAppServices } from '../app_plugin/types'; +import type { LensSerializedState } from '../react_embeddable/types'; const getQueryFromContext = ( context: VisualizeFieldContext | VisualizeEditorContext, @@ -149,6 +149,13 @@ export interface SetExecutionContextPayload { resolvedDateRange?: DateRange; } +export interface InitialAppState { + initialInput?: LensSerializedState; + redirectCallback?: (savedObjectId?: string) => void; + history?: History<unknown>; + inlineEditing?: boolean; +} + export const setState = createAction<Partial<LensAppState>>('lens/setState'); export const setExecutionContext = createAction<SetExecutionContextPayload>( 'lens/setExecutionContext' @@ -201,12 +208,7 @@ export const switchAndCleanDatasource = createAction<{ currentIndexPatternId?: string; }>('lens/switchAndCleanDatasource'); export const navigateAway = createAction<void>('lens/navigateAway'); -export const loadInitial = createAction<{ - initialInput?: LensEmbeddableInput; - redirectCallback?: (savedObjectId?: string) => void; - history?: History<unknown>; - inlineEditing?: boolean; -}>('lens/loadInitial'); +export const loadInitial = createAction<InitialAppState>('lens/loadInitial'); export const initEmpty = createAction( 'initEmpty', function prepare({ diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index a70b713787ce0..1514a508b8781 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -15,10 +15,10 @@ import { } from '../mocks'; import { Location, History } from 'history'; import { act } from 'react-dom/test-utils'; -import { LensEmbeddableInput } from '../embeddable'; -import { loadInitial } from './lens_slice'; +import { InitialAppState, loadInitial } from './lens_slice'; import { Filter } from '@kbn/es-query'; import faker from 'faker'; +import { DOC_TYPE } from '../../common/constants'; const history = { location: { @@ -35,26 +35,37 @@ const preloadedState = { }, }; -const defaultProps = { +const defaultProps: InitialAppState = { redirectCallback: jest.fn(), - initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput, + initialInput: { savedObjectId: defaultSavedObjectId }, history, }; +/** + * This is just a convenience wrapper around act & dispatch + * The loadInitial action is hijacked by a custom middleware which returns a Promise + * therefore we need to await before proceeding with all the checks + * The intent of this wrapper is to avoid confusion with this specific action + */ +async function loadInitialAppState( + store: ReturnType<typeof makeLensStore>['store'], + initialState: InitialAppState +) { + await act(async () => { + await store.dispatch(loadInitial(initialState)); + }); +} + describe('Initializing the store', () => { it('should initialize initial datasource', async () => { - const { store, deps } = await makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + const { store, deps } = makeLensStore({ preloadedState }); + await loadInitialAppState(store, defaultProps); expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled(); }); it('should have initialized the initial datasource and visualization', async () => { - const { store, deps } = await makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined })); - }); + const { store, deps } = makeLensStore({ preloadedState }); + await loadInitialAppState(store, { ...defaultProps, initialInput: undefined }); expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled(); expect(deps.datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled(); expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled(); @@ -65,7 +76,7 @@ describe('Initializing the store', () => { const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + services.attributeService.loadFromLibrary = jest.fn().mockResolvedValue({ attributes: { exactMatchDoc, visualizationType: 'testVis', @@ -107,16 +118,13 @@ describe('Initializing the store', () => { }, }); - const { store, deps } = await makeLensStore({ + const { store, deps } = makeLensStore({ storeDeps, preloadedState, }); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); const { datasourceMap } = deps; - expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled(); expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith( datasource1State, @@ -139,22 +147,17 @@ describe('Initializing the store', () => { describe('loadInitial', () => { it('does not load a document if there is no initial input', async () => { const { deps, store } = makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch( - loadInitial({ - ...defaultProps, - initialInput: undefined, - }) - ); + await loadInitialAppState(store, { + ...defaultProps, + initialInput: undefined, }); - expect(deps.lensServices.attributeService.unwrapAttributes).not.toHaveBeenCalled(); + + expect(deps.lensServices.attributeService.loadFromLibrary).not.toHaveBeenCalled(); }); it('starts new searchSessionId', async () => { - const { store } = await makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined })); - }); + const { store } = makeLensStore({ preloadedState }); + await loadInitialAppState(store, { ...defaultProps, initialInput: undefined }); expect(store.getState()).toEqual({ lens: expect.objectContaining({ searchSessionId: 'sessionId-1', @@ -163,7 +166,7 @@ describe('Initializing the store', () => { }); it('cleans datasource and visualization state properly when reloading', async () => { - const { store, deps } = await makeLensStore({ + const { store, deps } = makeLensStore({ preloadedState: { ...preloadedState, visualization: { @@ -187,13 +190,9 @@ describe('Initializing the store', () => { }), }); - await act(async () => { - await store.dispatch( - loadInitial({ - ...defaultProps, - initialInput: undefined, - }) - ); + await loadInitialAppState(store, { + ...defaultProps, + initialInput: undefined, }); expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled(); @@ -217,19 +216,17 @@ describe('Initializing the store', () => { it('loads a document and uses query and filters if initial input is provided', async () => { const { store, deps } = makeLensStore({ preloadedState }); - const mockFilters = 'some filters from the filter manager' as unknown as Filter[]; + const mockFilters = faker.lorem.words(3).split(' ') as unknown as Filter[]; jest .spyOn(deps.lensServices.data.query.filterManager, 'getFilters') .mockReturnValue(mockFilters); - await act(async () => { - store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); - expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); + expect(deps.lensServices.attributeService.loadFromLibrary).toHaveBeenCalledWith( + defaultSavedObjectId + ); expect(deps.lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ { query: { match_phrase: { src: 'test' } }, meta: { index: 'injected!' } }, @@ -237,8 +234,8 @@ describe('Initializing the store', () => { expect(store.getState()).toEqual({ lens: expect.objectContaining({ - persistedDoc: { ...defaultDoc, type: 'lens' }, - query: 'kuery', + persistedDoc: { ...defaultDoc, type: DOC_TYPE }, + query: defaultDoc.state.query, isLoading: false, activeDatasourceId: 'testDatasource', filters: mockFilters, @@ -249,68 +246,54 @@ describe('Initializing the store', () => { it('does not load documents on sequential renders unless the id changes', async () => { const { store, deps } = makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); - expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + expect(deps.lensServices.attributeService.loadFromLibrary).toHaveBeenCalledTimes(1); - await act(async () => { - await store.dispatch( - loadInitial({ - ...defaultProps, - initialInput: { savedObjectId: '5678' } as unknown as LensEmbeddableInput, - }) - ); + await loadInitialAppState(store, { + ...defaultProps, + initialInput: { savedObjectId: '5678' }, }); - expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); + expect(deps.lensServices.attributeService.loadFromLibrary).toHaveBeenCalledTimes(2); }); it('handles document load errors', async () => { const { store, deps } = makeLensStore({ preloadedState }); - deps.lensServices.attributeService.unwrapAttributes = jest + deps.lensServices.attributeService.loadFromLibrary = jest .fn() .mockRejectedValue('failed to load'); const redirectCallback = jest.fn(); - await act(async () => { - await store.dispatch(loadInitial({ ...defaultProps, redirectCallback })); - }); + await loadInitialAppState(store, { ...defaultProps, redirectCallback }); - expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); + expect(deps.lensServices.attributeService.loadFromLibrary).toHaveBeenCalledWith( + defaultSavedObjectId + ); expect(deps.lensServices.notifications.toasts.addDanger).toHaveBeenCalled(); expect(redirectCallback).toHaveBeenCalled(); }); it('redirects if saved object is an aliasMatch', async () => { const { store, deps } = makeLensStore({ preloadedState }); - deps.lensServices.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + deps.lensServices.attributeService.loadFromLibrary = jest.fn().mockResolvedValue({ attributes: { ...defaultDoc, }, - metaInfo: { - sharingSavedObjectProps: { - outcome: 'aliasMatch', - aliasTargetId: 'id2', - aliasPurpose: 'savedObjectConversion', - }, + sharingSavedObjectProps: { + outcome: 'aliasMatch', + aliasTargetId: 'id2', + aliasPurpose: 'savedObjectConversion', }, }); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); - expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); + expect(deps.lensServices.attributeService.loadFromLibrary).toHaveBeenCalledWith( + defaultSavedObjectId + ); expect(deps.lensServices.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ path: '#/edit/id2?search', aliasPurpose: 'savedObjectConversion', @@ -320,9 +303,7 @@ describe('Initializing the store', () => { it('adds to the recently accessed list on load', async () => { const { store, deps } = makeLensStore({ preloadedState }); - await act(async () => { - await store.dispatch(loadInitial(defaultProps)); - }); + await loadInitialAppState(store, defaultProps); expect(deps.lensServices.chrome.recentlyAccessed.add).toHaveBeenCalledWith( '/app/lens#/edit/1234', diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 2187302ae02e4..594b1b9632d62 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -7,11 +7,11 @@ import { createSelector } from '@reduxjs/toolkit'; import { FilterManager } from '@kbn/data-plugin/public'; -import { SavedObjectReference } from '@kbn/core/public'; -import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { isOfAggregateQueryType } from '@kbn/es-query'; import { LensState } from './types'; -import { Datasource, DatasourceMap, VisualizationMap } from '../types'; +import { DatasourceMap, VisualizationMap } from '../types'; import { getDatasourceLayers } from './utils'; +import { mergeToNewDoc } from './shared_logic'; export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc; export const selectQuery = (state: LensState) => state.lens.query; @@ -60,7 +60,7 @@ export const selectExecutionContext = createSelector( export const selectExecutionContextSearch = createSelector(selectExecutionContext, (res) => ({ now: res.now, - query: res.query, + query: isOfAggregateQueryType(res.query) ? undefined : res.query, timeRange: { from: res.dateRange.fromDate, to: res.dateRange.toDate, @@ -89,107 +89,7 @@ export const selectSavedObjectFormat = createSelector( extractFilterReferences: FilterManager['extract']; }>, ], - ( - persistedDoc, - visualization, - datasourceStates, - query, - filters, - activeDatasourceId, - adHocDataViews, - { datasourceMap, visualizationMap, extractFilterReferences } - ) => { - const activeVisualization = - visualization.state && visualization.activeId - ? visualizationMap[visualization.activeId] - : null; - const activeDatasource = - datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading - ? datasourceMap[activeDatasourceId] - : undefined; - - if (!activeDatasource || !activeVisualization) { - return; - } - - const activeDatasources: Record<string, Datasource> = Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ); - - const persistibleDatasourceStates: Record<string, unknown> = {}; - const references: SavedObjectReference[] = []; - const internalReferences: SavedObjectReference[] = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( - datasourceStates[id].state - ); - persistibleDatasourceStates[id] = persistableState; - savedObjectReferences.forEach((r) => { - if (r.type === 'index-pattern' && adHocDataViews[r.id]) { - internalReferences.push(r); - } else { - references.push(r); - } - }); - }); - - let persistibleVisualizationState = visualization.state; - if (activeVisualization.getPersistableState) { - const { state: persistableState, savedObjectReferences } = - activeVisualization.getPersistableState(visualization.state); - persistibleVisualizationState = persistableState; - savedObjectReferences.forEach((r) => { - if (r.type === 'index-pattern' && adHocDataViews[r.id]) { - internalReferences.push(r); - } else { - references.push(r); - } - }); - } - - const persistableAdHocDataViews = Object.fromEntries( - Object.entries(adHocDataViews).map(([id, dataView]) => { - const { references: dataViewReferences, state } = - DataViewPersistableStateService.extract(dataView); - references.push(...dataViewReferences); - return [id, state]; - }) - ); - - const adHocFilters = filters - .filter((f) => !references.some((r) => r.type === 'index-pattern' && r.id === f.meta.index)) - .map((f) => ({ ...f, meta: { ...f.meta, value: undefined } })); - - const referencedFilters = filters.filter((f) => - references.some((r) => r.type === 'index-pattern' && r.id === f.meta.index) - ); - - const { state: persistableFilters, references: filterReferences } = - extractFilterReferences(referencedFilters); - - references.push(...filterReferences); - - return { - savedObjectId: persistedDoc?.savedObjectId, - title: persistedDoc?.title || '', - description: persistedDoc?.description, - visualizationType: visualization.activeId, - type: 'lens', - references, - state: { - visualization: persistibleVisualizationState, - query, - filters: [...persistableFilters, ...adHocFilters], - datasourceStates: persistibleDatasourceStates, - internalReferences, - adHocDataViews: persistableAdHocDataViews, - }, - }; - } + mergeToNewDoc ); export const selectCurrentVisualization = createSelector( diff --git a/x-pack/plugins/lens/public/state_management/shared_logic.ts b/x-pack/plugins/lens/public/state_management/shared_logic.ts new file mode 100644 index 0000000000000..4e24d9f3fdaa0 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/shared_logic.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; +import { DataViewSpec, DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { AggregateQuery, Query, Filter } from '@kbn/es-query'; +import { FilterManager } from '@kbn/data-plugin/public'; +import { DOC_TYPE, INDEX_PATTERN_TYPE } from '../../common/constants'; +import { VisualizationState, DatasourceStates } from '.'; +import { LensDocument } from '../persistence'; +import { DatasourceMap, VisualizationMap, Datasource } from '../types'; + +// This piece of logic is shared between the main editor code base and the inline editor one within the embeddable +export function mergeToNewDoc( + persistedDoc: LensDocument | undefined, + visualization: VisualizationState, + datasourceStates: DatasourceStates, + query: AggregateQuery | Query, + filters: Filter[], + activeDatasourceId: string | null, + adHocDataViews: Record<string, DataViewSpec>, + { + datasourceMap, + visualizationMap, + extractFilterReferences, + }: { + datasourceMap: DatasourceMap; + visualizationMap: VisualizationMap; + extractFilterReferences: FilterManager['extract']; + } +) { + const activeVisualization = + visualization.state && visualization.activeId ? visualizationMap[visualization.activeId] : null; + const activeDatasource = + datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading + ? datasourceMap[activeDatasourceId] + : undefined; + + if (!activeDatasource || !activeVisualization) { + return; + } + + const activeDatasources: Record<string, Datasource> = Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ); + + const persistibleDatasourceStates: Record<string, unknown> = {}; + const references: SavedObjectReference[] = []; + const internalReferences: SavedObjectReference[] = []; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + datasourceStates[id].state + ); + persistibleDatasourceStates[id] = persistableState; + savedObjectReferences.forEach((r) => { + if (r.type === INDEX_PATTERN_TYPE && adHocDataViews[r.id]) { + internalReferences.push(r); + } else { + references.push(r); + } + }); + }); + + let persistibleVisualizationState = visualization.state; + if (activeVisualization.getPersistableState) { + const { state: persistableState, savedObjectReferences } = + activeVisualization.getPersistableState(visualization.state); + persistibleVisualizationState = persistableState; + savedObjectReferences.forEach((r) => { + if (r.type === INDEX_PATTERN_TYPE && adHocDataViews[r.id]) { + internalReferences.push(r); + } else { + references.push(r); + } + }); + } + + const persistableAdHocDataViews = Object.fromEntries( + Object.entries(adHocDataViews).map(([id, dataView]) => { + const { references: dataViewReferences, state } = + DataViewPersistableStateService.extract(dataView); + references.push(...dataViewReferences); + return [id, state]; + }) + ); + + const adHocFilters = filters + .filter((f) => !references.some((r) => r.type === INDEX_PATTERN_TYPE && r.id === f.meta.index)) + .map((f) => ({ ...f, meta: { ...f.meta, value: undefined } })); + + const referencedFilters = filters.filter((f) => + references.some((r) => r.type === INDEX_PATTERN_TYPE && r.id === f.meta.index) + ); + + const { state: persistableFilters, references: filterReferences } = + extractFilterReferences(referencedFilters); + + references.push(...filterReferences); + + return { + savedObjectId: persistedDoc?.savedObjectId, + title: persistedDoc?.title || '', + description: persistedDoc?.description, + visualizationType: visualization.activeId!, + type: DOC_TYPE, + references, + state: { + visualization: persistibleVisualizationState, + query, + filters: [...persistableFilters, ...adHocFilters], + datasourceStates: persistibleDatasourceStates, + internalReferences, + adHocDataViews: persistableAdHocDataViews, + }, + }; +} diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 1d683b655b58d..cc8f76118cf22 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -7,10 +7,10 @@ import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import type { EmbeddableEditorState } from '@kbn/embeddable-plugin/public'; -import type { Filter, Query } from '@kbn/es-query'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { SavedQuery } from '@kbn/data-plugin/public'; import type { MainHistoryLocationState } from '../../common/locator/locator'; -import type { Document } from '../persistence'; +import type { LensDocument } from '../persistence'; import type { TableInspectorAdapter } from '../editor_frame_service/types'; import type { DateRange } from '../../common/types'; @@ -54,14 +54,14 @@ export interface EditorFrameState extends PreviewState { isFullscreenDatasource?: boolean; } export interface LensAppState extends EditorFrameState { - persistedDoc?: Document; + persistedDoc?: LensDocument; // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. isLinkedToOriginatingApp?: boolean; isSaveable: boolean; isLoading: boolean; - query: Query; + query: Query | AggregateQuery; filters: Filter[]; savedQuery?: SavedQuery; searchSessionId: string; diff --git a/x-pack/plugins/lens/public/trigger_actions/convert_to_lens_action.ts b/x-pack/plugins/lens/public/trigger_actions/convert_to_lens_action.ts new file mode 100644 index 0000000000000..017cb64f9dd4b --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/convert_to_lens_action.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 { createAction } from '@kbn/ui-actions-plugin/public'; +import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; +import type { ApplicationStart } from '@kbn/core/public'; +import { APP_ID } from '../../common/constants'; +import type { VisualizeEditorContext } from '../types'; + +export const convertToLensActionFactory = + (id: string, displayName: string, originatingApp: string) => (application: ApplicationStart) => + createAction<{ [key: string]: VisualizeEditorContext }>({ + type: ACTION_CONVERT_TO_LENS, + id, + getDisplayName: () => displayName, + isCompatible: async () => !!application.capabilities.visualize.show, + execute: async (context: { [key: string]: VisualizeEditorContext }) => { + const table = Object.values(context.layers); + const payload = { + ...context, + layers: table, + isVisualizeAction: true, + }; + application.navigateToApp(APP_ID, { + state: { + type: ACTION_CONVERT_TO_LENS, + payload, + originatingApp, + }, + }); + }, + }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts index fd1ef4f746c41..c74486abfe8d0 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts @@ -8,30 +8,12 @@ import { DataViewsService } from '@kbn/data-views-plugin/public'; import { type EmbeddableApiContext } from '@kbn/presentation-publishing'; import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; -import { BehaviorSubject } from 'rxjs'; -import { DOC_TYPE } from '../../common/constants'; import { createOpenInDiscoverAction } from './open_in_discover_action'; import type { DiscoverAppLocator } from './open_in_discover_helpers'; +import { getLensApiMock } from '../react_embeddable/mocks'; describe('open in discover action', () => { - const compatibleEmbeddableApi = { - type: DOC_TYPE, - panelTitle: 'some title', - hidePanelTitle: false, - filters$: new BehaviorSubject([]), - query$: new BehaviorSubject({ query: 'test', language: 'kuery' }), - timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }), - getSavedVis: jest.fn(() => undefined), - canViewUnderlyingData$: new BehaviorSubject(true), - getFullAttributes: jest.fn(() => undefined), - getViewUnderlyingDataArgs: jest.fn(() => ({ - dataViewSpec: { id: 'index-pattern-id' }, - timeRange: { from: 'now-7d', to: 'now' }, - filters: [], - query: undefined, - columns: [], - })), - }; + const compatibleEmbeddableApi = getLensApiMock(); describe('compatibility check', () => { it('is incompatible with non-lens embeddables', async () => { @@ -49,6 +31,10 @@ describe('open in discover action', () => { }); it('is incompatible if user cant access Discover app', async () => { // setup + const lensApi = { + ...compatibleEmbeddableApi, + canViewUnderlyingData$: { getValue: jest.fn(() => true) }, + }; let hasDiscoverAccess = true; // make sure it would work if we had access to Discover @@ -58,7 +44,7 @@ describe('open in discover action', () => { {} as DataViewsService, hasDiscoverAccess ).isCompatible({ - embeddable: compatibleEmbeddableApi, + embeddable: lensApi, } as ActionExecutionContext<EmbeddableApiContext>) ).toBeTruthy(); @@ -70,7 +56,7 @@ describe('open in discover action', () => { {} as DataViewsService, hasDiscoverAccess ).isCompatible({ - embeddable: compatibleEmbeddableApi, + embeddable: lensApi, } as ActionExecutionContext<EmbeddableApiContext>) ).toBeFalsy(); }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index d9dccab616d5b..fa67aa74f9de3 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -10,7 +10,7 @@ import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-p import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; import type { DiscoverAppLocator } from './open_in_discover_helpers'; -import { LensApi } from '../embeddable'; +import { LensApi } from '../react_embeddable/types'; const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER'; @@ -41,7 +41,7 @@ export const createOpenInDiscoverAction = ( }, isCompatible: async (context: EmbeddableApiContext) => { const { isCompatible } = await getDiscoverHelpersAsync(); - return isCompatible({ + return await isCompatible({ hasDiscoverAccess, locator, dataViews, diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx index d9b8b93e28d07..199700af157d1 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx @@ -17,10 +17,10 @@ import { OpenInDiscoverDrilldown, } from './open_in_discover_drilldown'; import { DataViewsService } from '@kbn/data-views-plugin/public'; -import { LensApi } from '../embeddable'; +import { getLensApiMock } from '../react_embeddable/mocks'; jest.mock('./open_in_discover_helpers', () => ({ - isCompatible: jest.fn(() => true), + isCompatible: jest.fn().mockReturnValue(true), getHref: jest.fn(), })); @@ -63,19 +63,13 @@ describe('open in discover drilldown', () => { it('calls through to isCompatible helper', async () => { const filters: Filter[] = [{ meta: { disabled: false } }]; - await drilldown.isCompatible( - { openInNewTab: true }, - { embeddable: { type: 'lens' } as LensApi, filters } - ); + await drilldown.isCompatible({ openInNewTab: true }, { embeddable: getLensApiMock(), filters }); expect(isCompatible).toHaveBeenCalledWith(expect.objectContaining({ filters })); }); it('calls through to getHref helper', async () => { const filters: Filter[] = [{ meta: { disabled: false } }]; - await drilldown.execute( - { openInNewTab: true }, - { embeddable: { type: 'lens' } as LensApi, filters } - ); + await drilldown.execute({ openInNewTab: true }, { embeddable: getLensApiMock(), filters }); expect(getHref).toHaveBeenCalledWith(expect.objectContaining({ filters })); }); }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx index 6602dc4acb69f..0a8f7cb3bf5e2 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx @@ -21,7 +21,7 @@ import type { DataViewsService } from '@kbn/data-views-plugin/public'; import { apiIsOfType } from '@kbn/presentation-publishing'; import { DOC_TYPE } from '../../common/constants'; import type { DiscoverAppLocator } from './open_in_discover_helpers'; -import { LensApi } from '../embeddable'; +import { LensApi } from '../react_embeddable/types'; export const getDiscoverHelpersAsync = async () => await import('../async_services'); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts index 0a52ea6b4711f..3ad62f212e49b 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -10,7 +10,7 @@ import type { DataViewsService } from '@kbn/data-views-plugin/public'; import type { LocatorPublic } from '@kbn/share-plugin/public'; import type { SerializableRecord } from '@kbn/utility-types'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { isLensApi } from '../embeddable'; +import { isLensApi } from '../react_embeddable/type_guards'; interface DiscoverAppLocatorParams extends SerializableRecord { timeRange?: TimeRange; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 96cd0ab6877e3..6f875e49f160c 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -20,9 +20,8 @@ import type { Datasource, Visualization } from '../../types'; import type { LensPluginStartDependencies } from '../../plugin'; import { suggestionsApi } from '../../lens_suggestions_api'; import { generateId } from '../../id_generator'; -import { executeEditAction } from './edit_action_helpers'; -import { Embeddable } from '../../embeddable'; import type { EditorFrameService } from '../../editor_frame_service'; +import { LensApi } from '../..'; // datasourceMap and visualizationMap setters/getters export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< @@ -117,29 +116,21 @@ export async function executeCreateAction({ const attrs = getLensAttributesFromSuggestion({ filters: [], query: defaultEsqlQuery, - suggestion: firstSuggestion, + suggestion: { + ...firstSuggestion, + title: '', // when creating a new panel, we don't want to use the title from the suggestion + }, dataView, }); - const embeddable = await api.addNewPanel<object, Embeddable>({ + const embeddable = await api.addNewPanel<object, LensApi>({ panelType: 'lens', initialState: { attributes: attrs, id: generateId(), + isNewPanel: true, }, }); // open the flyout if embeddable has been created successfully - if (embeddable) { - const deletePanel = () => { - api.removePanel(embeddable.id); - }; - - executeEditAction({ - embeddable, - startDependencies: deps, - isNewPanel: true, - deletePanel, - ...core, - }); - } + embeddable?.onEdit?.(); } diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx deleted file mode 100644 index e9daa06b9ac07..0000000000000 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.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 React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; -import type { LensPluginStartDependencies } from '../../plugin'; -import { createMockStartDependencies } from '../../editor_frame_service/mocks'; -import { DOC_TYPE } from '../../../common/constants'; -import { ConfigureInLensPanelAction } from './edit_action'; - -describe('open config panel action', () => { - const coreStart = coreMock.createStart(); - const mockStartDependencies = - createMockStartDependencies() as unknown as LensPluginStartDependencies; - describe('compatibility check', () => { - it('is incompatible with non-lens embeddables', async () => { - const embeddable = { - type: 'NOT_LENS', - isTextBasedLanguage: () => true, - getInput: () => { - return { - viewMode: 'edit', - }; - }, - } as unknown as IEmbeddable; - const configurablePanelAction = new ConfigureInLensPanelAction( - mockStartDependencies, - coreStart - ); - - const isCompatible = await configurablePanelAction.isCompatible({ - embeddable, - } as ActionExecutionContext<{ embeddable: IEmbeddable }>); - - expect(isCompatible).toBeFalsy(); - }); - - it('is incompatible with input view mode', async () => { - const embeddable = { - type: 'NOT_LENS', - getInput: () => { - return { - viewMode: 'view', - }; - }, - } as unknown as IEmbeddable; - const configurablePanelAction = new ConfigureInLensPanelAction( - mockStartDependencies, - coreStart - ); - - const isCompatible = await configurablePanelAction.isCompatible({ - embeddable, - } as ActionExecutionContext<{ embeddable: IEmbeddable }>); - - expect(isCompatible).toBeFalsy(); - }); - - it('is compatible with text based language embeddable', async () => { - const embeddable = { - type: DOC_TYPE, - isTextBasedLanguage: () => true, - getInput: () => { - return { - viewMode: 'edit', - }; - }, - getIsEditable: () => true, - } as unknown as IEmbeddable; - const configurablePanelAction = new ConfigureInLensPanelAction( - mockStartDependencies, - coreStart - ); - - const isCompatible = await configurablePanelAction.isCompatible({ - embeddable, - } as ActionExecutionContext<{ embeddable: IEmbeddable }>); - - expect(isCompatible).toBeTruthy(); - }); - }); - describe('execution', () => { - it('opens flyout when executed', async () => { - const embeddable = { - type: DOC_TYPE, - isTextBasedLanguage: () => true, - getInput: () => { - return { - viewMode: 'edit', - }; - }, - getIsEditable: () => true, - openConfigPanel: jest.fn().mockResolvedValue(<span>Lens Config Panel Component</span>), - getRoot: () => { - return { - openOverlay: jest.fn(), - clearOverlays: jest.fn(), - }; - }, - } as unknown as IEmbeddable; - const configurablePanelAction = new ConfigureInLensPanelAction( - mockStartDependencies, - coreStart - ); - const spy = jest.spyOn(coreStart.overlays, 'openFlyout'); - - await configurablePanelAction.execute({ - embeddable, - } as ActionExecutionContext<{ embeddable: IEmbeddable }>); - - expect(spy).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx deleted file mode 100644 index 4ad23bc953d23..0000000000000 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.tsx +++ /dev/null @@ -1,57 +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 type { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { Action } from '@kbn/ui-actions-plugin/public'; -import type { LensPluginStartDependencies } from '../../plugin'; -import { isLensEmbeddable } from '../utils'; -import type { StartServices } from '../../types'; - -const ACTION_CONFIGURE_IN_LENS = 'ACTION_CONFIGURE_IN_LENS'; - -interface Context { - embeddable: IEmbeddable; -} - -export const getConfigureLensHelpersAsync = async () => await import('../../async_services'); - -export class ConfigureInLensPanelAction implements Action<Context> { - public type = ACTION_CONFIGURE_IN_LENS; - public id = ACTION_CONFIGURE_IN_LENS; - public order = 50; - - constructor( - protected readonly startDependencies: LensPluginStartDependencies, - protected readonly startServices: StartServices - ) {} - - public getDisplayName({ embeddable }: Context): string { - const language = isLensEmbeddable(embeddable) ? embeddable.getTextBasedLanguage() : undefined; - return i18n.translate('xpack.lens.app.editVisualizationLabel', { - defaultMessage: 'Edit {lang} visualization', - values: { lang: language }, - }); - } - - public getIconType() { - return 'pencil'; - } - - public async isCompatible({ embeddable }: Context) { - const { isEditActionCompatible } = await getConfigureLensHelpersAsync(); - return isEditActionCompatible(embeddable); - } - - public async execute({ embeddable }: Context) { - const { executeEditAction } = await getConfigureLensHelpersAsync(); - return executeEditAction({ - embeddable, - startDependencies: this.startDependencies, - ...this.startServices, - }); - } -} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts deleted file mode 100644 index 7ec70e687efe5..0000000000000 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts +++ /dev/null @@ -1,100 +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 './helpers.scss'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { tracksOverlays } from '@kbn/presentation-containers'; -import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { ViewMode } from '@kbn/embeddable-plugin/common'; -import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { isLensEmbeddable } from '../utils'; -import type { LensPluginStartDependencies } from '../../plugin'; -import { StartServices } from '../../types'; - -interface Context extends StartServices { - embeddable: IEmbeddable; - startDependencies: LensPluginStartDependencies; - isNewPanel?: boolean; - deletePanel?: () => void; -} - -export async function isEditActionCompatible(embeddable: IEmbeddable) { - if (!embeddable?.getInput) return false; - // display the action only if dashboard is on editable mode - const inDashboardEditMode = embeddable.getInput().viewMode === ViewMode.EDIT; - return Boolean(isLensEmbeddable(embeddable) && embeddable.getIsEditable() && inDashboardEditMode); -} - -type PanelConfigElement<T = {}> = React.ReactElement<T & { closeFlyout: () => void }>; - -const openInlineLensConfigEditor = ( - startServices: StartServices, - embeddable: IEmbeddable, - EmbeddableInlineConfigEditor: PanelConfigElement -) => { - const rootEmbeddable = embeddable.getRoot(); - const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined; - - const handle = startServices.overlays.openFlyout( - toMountPoint( - React.createElement(function InlineLensConfigEditor() { - React.useEffect(() => { - document.body.style.overflowY = 'hidden'; - - return () => { - document.body.style.overflowY = 'initial'; - }; - }, []); - - return React.cloneElement(EmbeddableInlineConfigEditor, { - closeFlyout: () => { - overlayTracker?.clearOverlays(); - handle.close(); - }, - }); - }), - startServices - ), - { - size: 's', - type: 'push', - paddingSize: 'm', - 'data-test-subj': 'customizeLens', - className: 'lnsConfigPanel__overlay', - hideCloseButton: true, - isResizable: true, - onClose: (overlayRef) => { - overlayTracker?.clearOverlays(); - overlayRef.close(); - }, - outsideClickCloses: true, - } - ); - - overlayTracker?.openOverlay(handle, { - focusedPanelId: embeddable.id, - }); -}; - -export async function executeEditAction({ - embeddable, - startDependencies, - isNewPanel, - deletePanel, - ...startServices -}: Context) { - const isCompatibleAction = await isEditActionCompatible(embeddable); - if (!isCompatibleAction || !isLensEmbeddable(embeddable)) { - throw new IncompatibleActionError(); - } - - const ConfigPanel = await embeddable.openConfigPanel(startDependencies, isNewPanel, deletePanel); - - if (ConfigPanel) { - openInlineLensConfigEditor(startServices, embeddable, ConfigPanel); - } -} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx index 7525f491e697a..1e1ab4cacff26 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx @@ -8,13 +8,17 @@ import type { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import type { LensPluginStartDependencies } from '../../../plugin'; import { createMockStartDependencies } from '../../../editor_frame_service/mocks'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; import { EditLensEmbeddableAction } from './in_app_embeddable_edit_action'; +import { TypedLensSerializedState } from '../../../react_embeddable/types'; +import { BehaviorSubject } from 'rxjs'; describe('inapp editing of Lens embeddable', () => { const core = coreMock.createStart(); const mockStartDependencies = createMockStartDependencies() as unknown as LensPluginStartDependencies; + + const renderComplete$ = new BehaviorSubject(false); + describe('compatibility check', () => { const attributes = { title: 'An extremely cool default document!', @@ -29,7 +33,7 @@ describe('inapp editing of Lens embeddable', () => { visualization: {}, }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - } as unknown as TypedLensByValueInput['attributes']; + } as TypedLensSerializedState['attributes']; it('is incompatible for ESQL charts and if ui setting for ES|QL is off', async () => { const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, core); const context = { @@ -37,6 +41,7 @@ describe('inapp editing of Lens embeddable', () => { lensEvent: { adapters: {}, embeddableOutput$: undefined, + renderComplete$, }, onUpdate: jest.fn(), }; @@ -61,6 +66,7 @@ describe('inapp editing of Lens embeddable', () => { lensEvent: { adapters: {}, embeddableOutput$: undefined, + renderComplete$, }, onUpdate: jest.fn(), }; @@ -86,6 +92,7 @@ describe('inapp editing of Lens embeddable', () => { lensEvent: { adapters: {}, embeddableOutput$: undefined, + renderComplete$, }, onUpdate: jest.fn(), }; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx index c132b5e88c6c4..74ffac32605be 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx @@ -12,8 +12,6 @@ import type { InlineEditLensEmbeddableContext } from './types'; const ACTION_EDIT_LENS_EMBEDDABLE = 'ACTION_EDIT_LENS_EMBEDDABLE'; -export const getAsyncHelpers = async () => await import('../../../async_services'); - export class EditLensEmbeddableAction implements Action<InlineEditLensEmbeddableContext> { public type = ACTION_EDIT_LENS_EMBEDDABLE; public id = ACTION_EDIT_LENS_EMBEDDABLE; @@ -35,7 +33,7 @@ export class EditLensEmbeddableAction implements Action<InlineEditLensEmbeddable } public async isCompatible({ attributes }: InlineEditLensEmbeddableContext) { - const { isEmbeddableEditActionCompatible } = await getAsyncHelpers(); + const { isEmbeddableEditActionCompatible } = await import('../../../async_services'); return isEmbeddableEditActionCompatible(this.core, attributes); } @@ -47,7 +45,7 @@ export class EditLensEmbeddableAction implements Action<InlineEditLensEmbeddable onApply, onCancel, }: InlineEditLensEmbeddableContext) { - const { executeEditEmbeddableAction } = await getAsyncHelpers(); + const { executeEditEmbeddableAction } = await import('../../../async_services'); if (attributes) { executeEditEmbeddableAction({ deps: this.startDependencies, diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx index 0a3dc8feebe0b..5eeaf01d2034f 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx @@ -4,18 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import type { CoreStart } from '@kbn/core/public'; +import type { CoreStart, OverlayRef } from '@kbn/core/public'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { BehaviorSubject } from 'rxjs'; +import '../helpers.scss'; +import { PublishingSubject } from '@kbn/presentation-publishing'; +import { generateId } from '../../../id_generator'; +import { setupPanelManagement } from '../../../react_embeddable/inline_editing/panel_management'; +import { prepareInlineEditPanel } from '../../../react_embeddable/inline_editing/setup_inline_editing'; +import { mountInlineEditPanel } from '../../../react_embeddable/inline_editing/mount'; +import type { TypedLensByValueInput, LensRuntimeState } from '../../../react_embeddable/types'; import type { LensPluginStartDependencies } from '../../../plugin'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; -import { extractReferencesFromState } from '../../../utils'; import type { LensChartLoadEvent } from './types'; +const asyncNoop = async () => {}; + export function isEmbeddableEditActionCompatible( core: CoreStart, attributes: TypedLensByValueInput['attributes'] @@ -49,106 +54,41 @@ export async function executeEditEmbeddableAction({ throw new IncompatibleActionError(); } - const { getEditLensConfiguration, getVisualizationMap, getDatasourceMap } = await import( - '../../../async_services' - ); - const visualizationMap = getVisualizationMap(); - const datasourceMap = getDatasourceMap(); - const query = attributes.state.query; - const activeDatasourceId = isOfAggregateQueryType(query) ? 'textBased' : 'formBased'; - - const onUpdatePanelState = ( - datasourceState: unknown, - visualizationState: unknown, - visualizationType?: string - ) => { - if (attributes.state) { - const datasourceStates = { - ...attributes.state.datasourceStates, - [activeDatasourceId]: datasourceState, - }; - - const references = extractReferencesFromState({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates: Object.fromEntries( - Object.entries(datasourceStates).map(([id, state]) => [id, { isLoading: false, state }]) - ), - visualizationState, - activeVisualization: visualizationType ? visualizationMap[visualizationType] : undefined, - }); - - const attrs = { - ...attributes, - state: { - ...attributes.state, - visualization: visualizationState, - datasourceStates, - }, - references, - visualizationType: visualizationType ?? attributes.visualizationType, - } as TypedLensByValueInput['attributes']; - - onUpdate(attrs); - } - }; - - const onUpdateSuggestion = (attrs: TypedLensByValueInput['attributes']) => { - const newAttributes = { - ...attributes, - ...attrs, - }; - onUpdate(newAttributes); - }; - - const Component = await getEditLensConfiguration(core, deps, visualizationMap, datasourceMap); - const ConfigPanel = ( - <Component - attributes={attributes} - updatePanelState={onUpdatePanelState} - lensAdapters={lensEvent?.adapters} - output$={lensEvent?.embeddableOutput$} - displayFlyoutHeader - datasourceId={activeDatasourceId} - onApplyCb={onApply} - onCancelCb={onCancel} - canEditTextBasedQuery={activeDatasourceId === 'textBased'} - updateSuggestion={onUpdateSuggestion} - hideTimeFilterInfo={true} - /> + const uuid = generateId(); + const isNewlyCreated$ = new BehaviorSubject<boolean>(false); + const panelManagementApi = setupPanelManagement(uuid, container, { + isNewlyCreated$, + setAsCreated: () => isNewlyCreated$.next(false), + }); + const openInlineEditor = prepareInlineEditPanel( + { attributes }, + () => ({ attributes }), + (newState: LensRuntimeState) => + onUpdate(newState.attributes as TypedLensByValueInput['attributes']), + { + dataLoading$: + lensEvent?.dataLoading$ ?? + (new BehaviorSubject(undefined) as PublishingSubject<boolean | undefined>), + isNewlyCreated$, + }, + panelManagementApi, + { + getInspectorAdapters: () => lensEvent?.adapters, + inspect(): OverlayRef { + return { close: asyncNoop, onClose: Promise.resolve() }; + }, + closeInspector: asyncNoop, + adapters$: new BehaviorSubject(lensEvent?.adapters), + }, + { coreStart: core, ...deps } ); - // in case an element is given render the component in the container, - // otherwise a flyout will open - if (container) { - ReactDOM.render(ConfigPanel, container); - } else { - const handle = core.overlays.openFlyout( - toMountPoint( - React.cloneElement(ConfigPanel, { - closeFlyout: () => { - handle.close(); - }, - }), - core - ), - { - className: 'lnsConfigPanel__overlay', - size: 's', - 'data-test-subj': 'customizeLens', - type: 'push', - paddingSize: 'm', - hideCloseButton: true, - onClose: (overlayRef) => { - overlayRef.close(); - }, - outsideClickCloses: true, - } - ); + const ConfigPanel = await openInlineEditor({ + onApply, + onCancel, + }); + if (ConfigPanel) { + // no need to pass the uuid in this use case + mountInlineEditPanel(ConfigPanel, core, undefined, undefined, container); } } diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts index d86f05d4156e9..0176ef4ee9e8c 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts @@ -5,9 +5,8 @@ * 2.0. */ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; -import type { EmbeddableOutput } from '@kbn/embeddable-plugin/public'; -import type { Observable } from 'rxjs'; -import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import { PublishingSubject } from '@kbn/presentation-publishing'; +import type { TypedLensByValueInput } from '../../../react_embeddable/types'; export interface LensChartLoadEvent { /** @@ -15,9 +14,9 @@ export interface LensChartLoadEvent { */ adapters: Partial<DefaultInspectorAdapters>; /** - * Observable of the lens embeddable output + * Observable to track embeddable loading state */ - embeddableOutput$?: Observable<EmbeddableOutput>; + dataLoading$?: PublishingSubject<boolean | undefined>; } export interface InlineEditLensEmbeddableContext { diff --git a/x-pack/plugins/lens/public/trigger_actions/utils.ts b/x-pack/plugins/lens/public/trigger_actions/utils.ts deleted file mode 100644 index 527f1adcf7629..0000000000000 --- a/x-pack/plugins/lens/public/trigger_actions/utils.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. - */ -import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import type { Embeddable } from '../embeddable'; -import { DOC_TYPE } from '../../common/constants'; - -export function isLensEmbeddable(embeddable: IEmbeddable): embeddable is Embeddable { - return embeddable.type === DOC_TYPE; -} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 5b5e33564cc7d..d6dbccc492a6b 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -10,7 +10,7 @@ import type { CoreStart, SavedObjectReference, ResolvedSimpleSavedObject } from import type { ColorMapping, PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject, ReactElement } from 'react'; -import type { Filter, TimeRange } from '@kbn/es-query'; +import type { Query, AggregateQuery, Filter, TimeRange } from '@kbn/es-query'; import type { ExpressionAstExpression, IInterpreterRenderHandlers, @@ -22,7 +22,6 @@ import type { NavigateToLensContext, SeriesType, } from '@kbn/visualizations-plugin/common'; -import type { Query } from '@kbn/es-query'; import type { UiActionsStart, RowClickContext, @@ -63,7 +62,7 @@ import { import type { LensInspector } from './lens_inspector_service'; import type { DataViewsState } from './state_management/types'; import type { IndexPatternServiceAPI } from './data_views_service/service'; -import type { Document } from './persistence/saved_object_store'; +import type { LensDocument } from './persistence/saved_object_store'; import { TableInspectorAdapter } from './editor_frame_service/types'; export type StartServices = Pick< @@ -140,8 +139,8 @@ export interface EditorFrameInstance { export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation - registerDatasource: <T, P>( - datasource: Datasource<T, P> | (() => Promise<Datasource<T, P>>) + registerDatasource: <T, P, Q>( + datasource: Datasource<T, P, Q> | (() => Promise<Datasource<T, P, Q>>) ) => void; registerVisualization: <T, P, ExtraAppendLayerArg>( visualization: @@ -322,8 +321,11 @@ export type AddUserMessages = (messages: UserMessage[]) => () => void; /** * Interface for the datasource registry + * T type: runtime Lens state + * P type: persisted Lens state + * Q type: Query type (useful to filter form vs text based queries) */ -export interface Datasource<T = unknown, P = unknown> { +export interface Datasource<T = unknown, P = unknown, Q = Query | AggregateQuery> { id: string; alias?: string[]; @@ -382,7 +384,7 @@ export interface Datasource<T = unknown, P = unknown> { LayerSettingsComponent?: ( props: DatasourceLayerSettingsProps<T> ) => React.ReactElement<DatasourceLayerSettingsProps<T>> | null; - DataPanelComponent: (props: DatasourceDataPanelProps<T>) => JSX.Element | null; + DataPanelComponent: (props: DatasourceDataPanelProps<T, Q>) => JSX.Element | null; DimensionTriggerComponent: (props: DatasourceDimensionTriggerProps<T>) => JSX.Element | null; DimensionEditorComponent: ( props: DatasourceDimensionEditorProps<T> @@ -579,7 +581,7 @@ export interface DatasourceLayerSettingsProps<T = unknown> { setState: StateSetter<T>; } -export interface DatasourceDataPanelProps<T = unknown> { +export interface DatasourceDataPanelProps<T = unknown, Q = Query | AggregateQuery> { state: T; setState: StateSetter<T, { applyImmediately?: boolean }>; showNoDataPopover: () => void; @@ -587,7 +589,7 @@ export interface DatasourceDataPanelProps<T = unknown> { CoreStart, 'http' | 'notifications' | 'uiSettings' | 'overlays' | 'theme' | 'application' | 'docLinks' >; - query: Query; + query: Q; dateRange: DateRange; filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; @@ -946,7 +948,7 @@ export interface VisualizationSuggestion<T = unknown> { export type DatasourceLayers = Partial<Record<string, DatasourcePublicAPI>>; export interface FramePublicAPI { - query: Query; + query: Query | AggregateQuery; filters: Filter[]; datasourceLayers: DatasourceLayers; dateRange: DateRange; @@ -1456,10 +1458,10 @@ export type LensTopNavMenuEntryGenerator = (props: { visualizationId: string; datasourceStates: Record<string, { state: unknown }>; visualizationState: unknown; - query: Query; + query: Query | AggregateQuery; filters: Filter[]; initialContext?: VisualizeFieldContext | VisualizeEditorContext; - currentDoc: Document | undefined; + currentDoc: LensDocument | undefined; }) => undefined | TopNavMenuData; export interface LensCellValueAction { @@ -1473,3 +1475,5 @@ export interface LensCellValueAction { export type GetCompatibleCellValueActions = ( data: CellValueContext['data'] ) => Promise<LensCellValueAction[]>; + +export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {}; diff --git a/x-pack/plugins/lens/public/user_messages_ids.ts b/x-pack/plugins/lens/public/user_messages_ids.ts index a57e5f871cbf9..1bd15a642ba30 100644 --- a/x-pack/plugins/lens/public/user_messages_ids.ts +++ b/x-pack/plugins/lens/public/user_messages_ids.ts @@ -95,3 +95,6 @@ export const GAUGE_METRIC_GT_MAX = 'gauge_metric_gt_max'; export const GAUGE_GOAL_GT_MAX = 'gauge_goal_gt_max'; export const TEXT_BASED_LANGUAGE_ERROR = 'text_based_lang_error'; + +export const URL_CONFLICT = 'url-conflict'; +export const MISSING_TIME_RANGE_ON_EMBEDDABLE = 'missing-time-range-on-embeddable'; diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 43129161adde1..0b0c7037d076e 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -20,7 +20,7 @@ import { ISearchStart } from '@kbn/data-plugin/public'; import type { DraggingIdentifier, DropType } from '@kbn/dom-drag-drop'; import { getAbsoluteTimeRange } from '@kbn/data-plugin/common'; import { DateRange } from '../common/types'; -import type { Document } from './persistence/saved_object_store'; +import type { LensDocument } from './persistence/saved_object_store'; import { Datasource, DatasourceMap, @@ -100,7 +100,7 @@ export function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } -export function getActiveDatasourceIdFromDoc(doc?: Document) { +export function getActiveDatasourceIdFromDoc(doc?: LensDocument) { if (!doc) { return null; } @@ -109,14 +109,14 @@ export function getActiveDatasourceIdFromDoc(doc?: Document) { return firstDatasourceFromDoc || null; } -export function getActiveVisualizationIdFromDoc(doc?: Document) { +export function getActiveVisualizationIdFromDoc(doc?: LensDocument) { if (!doc) { return null; } return doc.visualizationType || null; } -export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Document) => { +export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: LensDocument) => { return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 5f08ce1b9ced6..74e5a2a0d7ac9 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -7,15 +7,22 @@ import { i18n } from '@kbn/i18n'; import type { VisTypeAlias } from '@kbn/visualizations-plugin/public'; -import { getBasePath, getEditPath } from '../common/constants'; +import { + APP_ID, + getBasePath, + getEditPath, + LENS_EMBEDDABLE_TYPE, + LENS_ICON, + STAGE_ID, +} from '../common/constants'; import { getLensClient } from './persistence/lens_client'; export const getLensAliasConfig = (): VisTypeAlias => ({ alias: { path: getBasePath(), - app: 'lens', + app: APP_ID, }, - name: 'lens', + name: APP_ID, promotion: true, title: i18n.translate('xpack.lens.visTypeAlias.title', { defaultMessage: 'Lens', @@ -25,11 +32,11 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ 'Create visualizations using an intuitive drag-and-drop interface. Smart suggestions help you follow best practices and find the chart types that best match your data.', }), order: 60, - icon: 'lensApp', - stage: 'production', + icon: LENS_ICON, + stage: STAGE_ID, appExtensions: { visualizations: { - docTypes: ['lens'], + docTypes: [LENS_EMBEDDABLE_TYPE], searchFields: ['title^3'], clientOptions: { update: { overwrite: true } }, client: getLensClient, @@ -43,10 +50,10 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ updatedAt, managed, editor: { editUrl: getEditPath(id), editApp: 'lens' }, - icon: 'lensApp', - stage: 'production', + icon: LENS_ICON, + stage: STAGE_ID, savedObjectType: type, - type: 'lens', + type: LENS_EMBEDDABLE_TYPE, typeTitle: i18n.translate('xpack.lens.visTypeAlias.type', { defaultMessage: 'Lens' }), }; }, diff --git a/x-pack/plugins/lens/public/visualization_container.scss b/x-pack/plugins/lens/public/visualization_container.scss index 3eb4061f8b931..488b138cb0693 100644 --- a/x-pack/plugins/lens/public/visualization_container.scss +++ b/x-pack/plugins/lens/public/visualization_container.scss @@ -27,7 +27,7 @@ } // Make the visualization modifiers icon appear only on panel hover -.embPanel__content:hover .lnsEmbeddablePanelFeatureList_button { +.embPanel__content:hover .lnsPanelFeatureList_button { color: $euiTextColor; background: $euiColorEmptyShade; transition: color $euiAnimSpeedSlow, background $euiAnimSpeedSlow; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.scss b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.scss index 0f0ed53a10021..117c6797c3620 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.scss +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.scss @@ -1,5 +1,10 @@ .lnsDataTableContainer { height: 100%; + + // EUI issue to make background transparent https://github.com/elastic/eui/issues/8136 + .euiDataGrid__content { + background: transparent; + } } .lnsTableCell--multiline { diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index 0633c13b8097a..f30686a44a6ad 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -70,7 +70,8 @@ export const DataContext = React.createContext<DataContextType>({}); const gridStyle: EuiDataGridStyle = { border: 'horizontal', - header: 'underline', + header: 'shade', + footer: 'shade', }; export const DEFAULT_PAGE_SIZE = 10; diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index db249f19f3614..39ec693856441 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -88,7 +88,6 @@ "@kbn/core-plugins-server", "@kbn/esql", "@kbn/field-utils", - "@kbn/panel-loader", "@kbn/shared-ux-button-toolbar", "@kbn/cell-actions", "@kbn/presentation-containers", @@ -111,9 +110,14 @@ "@kbn/licensing-plugin", "@kbn/react-kibana-context-render", "@kbn/react-kibana-mount", + "@kbn/embeddable-enhanced-plugin", "@kbn/es-types", "@kbn/esql-datagrid", "@kbn/transpose-utils", + "@kbn/core-application-browser", + "@kbn/core-chrome-browser", + "@kbn/core-capabilities-common", + "@kbn/presentation-panel-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/style_error.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/style_error.tsx new file mode 100644 index 0000000000000..06811f79517aa --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/style_error.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DynamicStyleProperty } from '../../properties/dynamic_style_property'; + +interface Props { + error: Error; + style: DynamicStyleProperty<object>; +} + +export const StyleError = ({ error, style }: Props) => { + const [label, setLabel] = useState(''); + const styleName = style.getDisplayStyleName(); + + useEffect(() => { + let canceled = false; + const getLabel = async () => { + const field = style.getField(); + if (!field) { + return; + } + + const fieldLabel = await field.getLabel(); + + if (canceled) { + return; + } + + setLabel(fieldLabel); + }; + + getLabel(); + + return () => { + canceled = true; + }; + }, [style]); + + return ( + <div> + <EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiToolTip position="top" title={styleName} content={label}> + <EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}> + <small> + <strong>{label}</strong> + </small> + </EuiText> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiCallOut + title={i18n.translate('xpack.maps.vectorStyleLegend.fetchStyleMetaDataError', { + defaultMessage: 'Unable to fetch style meta data', + })} + color="warning" + iconType="warning" + > + <p>{error.message}</p> + </EuiCallOut> + </EuiFlexGroup> + </div> + ); +}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 60bcd05c9f738..afe1bbe194636 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -8,6 +8,11 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; +import { StyleError } from './style_error'; +import { + DynamicStyleProperty, + IDynamicStyleProperty, +} from '../../properties/dynamic_style_property'; import { FIELD_ORIGIN } from '../../../../../../common/constants'; import { Mask } from '../../../../layers/vector_layer/mask'; import { IStyleProperty } from '../../properties/style_property'; @@ -33,12 +38,22 @@ export function VectorStyleLegend({ const legendRows = []; for (let i = 0; i < styles.length; i++) { - const row = styles[i].renderLegendDetailRow({ - isLinesOnly, - isPointsOnly, - symbolId, - svg, - }); + const styleMetaDataRequest = styles[i].isDynamic() + ? (styles[i] as IDynamicStyleProperty<object>).getStyleMetaDataRequest() + : undefined; + + const error = styleMetaDataRequest?.getError(); + + const row = error ? ( + <StyleError error={error} style={styles[i] as DynamicStyleProperty<object>} /> + ) : ( + styles[i].renderLegendDetailRow({ + isLinesOnly, + isPointsOnly, + symbolId, + svg, + }) + ); legendRows.push( <div key={i} className="vectorStyleLegendSpacer"> 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 e3751d9424dd0..7790ebb0956bf 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 @@ -9,6 +9,7 @@ import _ from 'lodash'; import React from 'react'; import { FeatureCollection } from 'geojson'; import type { FeatureIdentifier, Map as MbMap } from '@kbn/mapbox-gl'; +import { DataRequest } from '../../../util/data_request'; import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { @@ -97,6 +98,7 @@ export interface IDynamicStyleProperty<T> extends IStyleProperty<T> { mbMap: MbMap, mbSourceId: string ): boolean; + getStyleMetaDataRequest(): DataRequest | undefined; } export class DynamicStyleProperty<T extends object> @@ -122,6 +124,11 @@ export class DynamicStyleProperty<T extends object> this._getFieldFormatter = getFieldFormatter; } + getStyleMetaDataRequest() { + const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName()); + return dataRequestId ? this._layer.getDataRequest(dataRequestId) : undefined; + } + getValueSuggestions = async (query: string) => { return this._field === null ? [] @@ -147,12 +154,7 @@ export class DynamicStyleProperty<T extends object> } _getRangeFieldMetaFromStyleMetaRequest(): RangeFieldMeta | null { - const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName()); - if (!dataRequestId) { - return null; - } - - const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); + const styleMetaDataRequest = this.getStyleMetaDataRequest(); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return null; } @@ -177,12 +179,7 @@ export class DynamicStyleProperty<T extends object> return null; } - const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName()); - if (!dataRequestId) { - return null; - } - - const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); + const styleMetaDataRequest = this.getStyleMetaDataRequest(); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return null; } @@ -202,12 +199,7 @@ export class DynamicStyleProperty<T extends object> } _getCategoryFieldMetaFromStyleMetaRequest() { - const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName()); - if (!dataRequestId) { - return []; - } - - const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId); + const styleMetaDataRequest = this.getStyleMetaDataRequest(); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return []; } diff --git a/x-pack/plugins/maps/public/classes/util/data_request.tsx b/x-pack/plugins/maps/public/classes/util/data_request.tsx index 4554761ac0f4e..12b9e6a66045c 100644 --- a/x-pack/plugins/maps/public/classes/util/data_request.tsx +++ b/x-pack/plugins/maps/public/classes/util/data_request.tsx @@ -54,6 +54,10 @@ export class DataRequest { return this._descriptor.dataRequestToken; } + getError(): Error | undefined { + return this._descriptor.error; + } + renderError(): ReactNode { if (!this._descriptor.error) { return null; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts index 99e605fd50864..cda0e842f2c95 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts @@ -14,7 +14,7 @@ import type { import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimefilterContract } from '@kbn/data-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import type { Filter, Query } from '@kbn/es-query'; +import { isOfAggregateQueryType, type Filter, type Query } from '@kbn/es-query'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { LensApi } from '@kbn/lens-plugin/public'; import type { JobCreatorType } from '../common/job_creator'; @@ -198,6 +198,10 @@ export class QuickLensJobCreator extends QuickJobCreatorBase { bucketSpan: string, layerIndex?: number ) { + // @TODO: ask ML team to check if ES|QL query here is ok + if (isOfAggregateQueryType(chartInfo.query)) { + throw new Error('Cannot create job, query is of aggregate type'); + } const compatibleLayers = chartInfo.layers.filter(isCompatibleLayer); const selectedLayer = diff --git a/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.test.ts b/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.test.ts index 34875b893a867..4193251d76f3a 100644 --- a/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.test.ts +++ b/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.test.ts @@ -627,6 +627,297 @@ describe('DeploymentParamsMapper', () => { }, }); }); + + describe('mapApiToUiDeploymentParams', () => { + it('should map API params to UI correctly', () => { + // Optimized for search + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 2, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'medium', + }); + + // Lower value + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 1, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'medium', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 8, + number_of_allocations: 2, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'medium', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 2, + number_of_allocations: 1, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'low', + }); + + // Exact match + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 8, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'high', + }); + + // Higher value + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 12, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'high', + }); + + // Lower value + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 5, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'high', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + number_of_allocations: 6, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: false, + vCPUUsage: 'high', + }); + + // Optimized for ingest + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 1, + number_of_allocations: 1, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForIngest', + adaptiveResources: false, + vCPUUsage: 'low', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 1, + number_of_allocations: 2, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForIngest', + adaptiveResources: false, + vCPUUsage: 'low', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 1, + number_of_allocations: 6, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForIngest', + adaptiveResources: false, + vCPUUsage: 'medium', + }); + }); + + it('should map API params to UI correctly with adaptive resources', () => { + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 8, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 2, + max_number_of_allocations: 2, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: true, + vCPUUsage: 'medium', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 2, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 2, + max_number_of_allocations: 2, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: true, + vCPUUsage: 'medium', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 1, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 1, + max_number_of_allocations: 1, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForIngest', + adaptiveResources: true, + vCPUUsage: 'low', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 2, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 1, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: true, + vCPUUsage: 'low', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 1, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 64, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForIngest', + adaptiveResources: true, + vCPUUsage: 'high', + }); + + expect( + mapper.mapApiToUiDeploymentParams({ + model_id: modelId, + deployment_id: 'test-deployment', + priority: 'normal', + threads_per_allocation: 16, + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 12, + }, + } as unknown as MlTrainedModelAssignmentTaskParametersAdaptive) + ).toEqual({ + deploymentId: 'test-deployment', + optimized: 'optimizedForSearch', + adaptiveResources: true, + vCPUUsage: 'high', + }); + }); + }); }); }); }); diff --git a/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.ts b/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.ts index ecb8a06198b1c..96ba5f0755caa 100644 --- a/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.ts +++ b/x-pack/plugins/ml/public/application/model_management/deployment_params_mapper.ts @@ -25,7 +25,7 @@ type VCPUBreakpoints = Record< max: number; /** * Static value is used for the number of vCPUs when the adaptive resources are disabled. - * Not allowed in certain environments. + * Not allowed in certain environments, Obs and Security serverless projects. */ static?: number; } @@ -89,6 +89,7 @@ export class DeploymentParamsMapper { ) { /** * Initial value can be different for serverless and ESS with autoscaling. + * Also not available with 0 ML active nodes. */ const maxSingleMlNodeProcessors = this.mlServerLimits.max_single_ml_node_processors; @@ -236,18 +237,25 @@ export class DeploymentParamsMapper { ? input.adaptive_allocations!.max_number_of_allocations! : input.number_of_allocations); + // The deployment can be created via API with a number of allocations that do not exactly match our vCPU ranges. + // In this case, we should find the closest vCPU range that does not exceed the max or static value of the range. const [vCPUUsage] = Object.entries(this.vCpuBreakpoints) - .reverse() - .find(([key, val]) => vCPUs >= val.min) as [ - DeploymentParamsUI['vCPUUsage'], - { min: number; max: number } - ]; + .filter(([, range]) => vCPUs <= (adaptiveResources ? range.max : range.static!)) + .reduce( + (prev, curr) => { + const prevValue = adaptiveResources ? prev[1].max : prev[1].static!; + const currValue = adaptiveResources ? curr[1].max : curr[1].static!; + return Math.abs(vCPUs - prevValue) <= Math.abs(vCPUs - currValue) ? prev : curr; + }, + // in case allocation params exceed the max value of the high range + ['high', this.vCpuBreakpoints.high] + ); return { deploymentId: input.deployment_id, optimized, adaptiveResources, - vCPUUsage, + vCPUUsage: vCPUUsage as DeploymentParamsUI['vCPUUsage'], }; } } diff --git a/x-pack/plugins/ml/server/lib/node_utils.ts b/x-pack/plugins/ml/server/lib/node_utils.ts index d8098e3c43f42..9c5c0348f03da 100644 --- a/x-pack/plugins/ml/server/lib/node_utils.ts +++ b/x-pack/plugins/ml/server/lib/node_utils.ts @@ -33,7 +33,7 @@ export async function getMlNodeCount(client: IScopedClusterClient): Promise<MlNo return { count, lazyNodeCount }; } -export async function getLazyMlNodeCount(client: IScopedClusterClient) { +export async function getLazyMlNodeCount(client: IScopedClusterClient): Promise<number> { const body = await client.asInternalUser.cluster.getSettings( { include_defaults: true, diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d4127a7428397..150fc3c75a109 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -14,7 +14,7 @@ import { mlLog } from '../lib/log'; import { capabilitiesProvider } from '../lib/capabilities'; import { spacesUtilsProvider } from '../lib/spaces_utils'; import type { RouteInitialization, SystemRouteDeps } from '../types'; -import { getMlNodeCount } from '../lib/node_utils'; +import { getLazyMlNodeCount, getMlNodeCount } from '../lib/node_utils'; /** * System routes @@ -187,10 +187,15 @@ export function systemRoutes( let isMlAutoscalingEnabled = false; try { - await client.asInternalUser.autoscaling.getAutoscalingPolicy({ name: 'ml' }); + // kibana_system user does not have the manage_autoscaling cluster privilege. + // perform this check as a current user. + await client.asCurrentUser.autoscaling.getAutoscalingPolicy({ name: 'ml' }); isMlAutoscalingEnabled = true; } catch (e) { - // If doesn't exist, then keep the false + // If ml autoscaling policy doesn't exist or the user does not have privileges to fetch it, + // check the number of lazy ml nodes to determine if autoscaling is enabled. + const lazyMlNodeCount = await getLazyMlNodeCount(client); + isMlAutoscalingEnabled = lazyMlNodeCount > 0; } return response.ok({ diff --git a/x-pack/plugins/observability_solution/apm/dev_docs/testing.md b/x-pack/plugins/observability_solution/apm/dev_docs/testing.md index 02ff43d37bf75..aeb9435a2e113 100644 --- a/x-pack/plugins/observability_solution/apm/dev_docs/testing.md +++ b/x-pack/plugins/observability_solution/apm/dev_docs/testing.md @@ -3,7 +3,9 @@ We've got three ways of testing our code: - Unit testing with Jest -- API testing +- Integration Tests + - Deployment specific (stateful or serverless) API testing + - Deployment-agnostic (both stateful and serverless) testing - End-to-end testing (with Cypress) API tests are usually preferred. They're stable and reasonably quick, and give a good approximation of real-world usage. @@ -73,7 +75,59 @@ node x-pack/plugins/observability_solution/apm/scripts/test/api --runner --basic #### API Test tips -- For data generation in API tests have a look at the [kbn-apm-synthtrace](../../../../packages/kbn-apm-synthtrace/README.md) package +- For data generation in API tests have a look at the [kbn-apm-synthtrace](../../../../../packages/kbn-apm-synthtrace/README.md) package +- For debugging access Elasticsearch on http://localhost:9220 and Kibana on http://localhost:5620 (`elastic` / `changeme`) + +--- + +## Deployment-agnostic Tests (dat) + +| Option | Description | +| ------------ | ----------------------------------------------- | +| --serverless | Loads serverless configuration | +| --stateful | Loads stateful configuration | +| --server | Only start ES and Kibana | +| --runner | Only run tests | +| --grep | Specify the specs to run | +| --grep-files | Specify the files to run | +| --inspect | Add --inspect-brk flag to the ftr for debugging | +| --times | Repeat the test n number of times | + +Deployment-agnostic tests are located in [`x-pack/test/deployment_agnostic/apis/observability/apm/index.ts`](../../../../test/api_integration/deployment_agnostic/apis/observability/apm/index.ts). + +#### Start server and run test (single process) + +``` +node x-pack/plugins/observability_solution/apm/scripts/test/dat [--serverless/--stateful] [--help] +``` + +The above command will start an ES instance on http://localhost:9220, a Kibana instance on http://localhost:5620 and run the api tests. +Once the tests finish, the instances will be terminated. + +#### Start server and run test (separate processes) + +```sh + +# start server +node x-pack/plugins/observability_solution/apm/scripts/test/dat --server --stateful + +# run tests +node x-pack/plugins/observability_solution/apm/scripts/test/dat --runner --stateful --grep-files=error_group_list +``` + +### Update snapshots (from Kibana root) + +To update snapshots append `--updateSnapshots` to the `--runner` command: + +``` +node x-pack/plugins/observability_solution/apm/scripts/test/dat --runner --stateful --updateSnapshots +``` + +(The test server needs to be running) + +#### API Test tips + +- For data generation in Deployment-agnostic tests have a look at the [kbn-apm-synthtrace](../../../../../packages/kbn-apm-synthtrace/README.md) package - For debugging access Elasticsearch on http://localhost:9220 and Kibana on http://localhost:5620 (`elastic` / `changeme`) --- diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress.config.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress.config.ts index feb0a630043d4..8dad8724264ee 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress.config.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress.config.ts @@ -31,5 +31,6 @@ export default defineCypressConfig({ baseUrl: 'http://localhost:5601', supportFile: './cypress/support/e2e.ts', specPattern: './cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + experimentalMemoryManagement: true, }, }); diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts index 6198ba8c5d05f..119c545eb5db8 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts @@ -91,9 +91,11 @@ describe('Service inventory', () => { it('with the correct environment when changing the environment', () => { cy.wait(mainAliasNames); - cy.getByTestSubj('environmentFilter').type('{selectall}production'); - - cy.contains('button', 'production').click(); + cy.getByTestSubj('environmentFilter').find('input').click(); + cy.getByTestSubj('comboBoxOptionsList environmentFilter-optionsList').should('be.visible'); + cy.getByTestSubj('comboBoxOptionsList environmentFilter-optionsList') + .contains('button', 'production') + .click(); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: mainAliasNames, diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_overview/service_overview.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_overview/service_overview.cy.ts index 4840037cafb83..509f857b612bd 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_overview/service_overview.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/service_overview/service_overview.cy.ts @@ -191,19 +191,10 @@ describe('Service Overview', () => { it('with the correct environment when changing the environment', () => { cy.wait(aliasNames); - cy.intercept('GET', 'internal/apm/suggestions?*').as('suggestionsRequest'); - - cy.getByTestSubj('environmentFilter') - .find('input') - .type('{selectall}production', { force: true }); - - cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: ['@suggestionsRequest'], - value: 'fieldValue=production', - }); - + cy.getByTestSubj('environmentFilter').find('input').click(); cy.getByTestSubj('comboBoxOptionsList environmentFilter-optionsList') - .contains('production') + .should('be.visible') + .contains('button', 'production') .click({ force: true }); cy.expectAPIsToHaveBeenCalledWith({ diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/settings/agent_configurations.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/settings/agent_configurations.cy.ts index d5bb161512a9b..f72d331dab20c 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/settings/agent_configurations.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/settings/agent_configurations.cy.ts @@ -127,16 +127,18 @@ describe('Agent configuration', () => { 'serviceEnvironmentApi' ); cy.contains('Create configuration').click(); - cy.getByTestSubj('serviceNameComboBox').click().type('opbeans-node').type('{enter}'); - - cy.contains('opbeans-node').realClick(); + cy.getByTestSubj('serviceNameComboBox').find('input').click(); + cy.getByTestSubj('serviceNameComboBox').type('opbeans-node{enter}'); cy.wait('@serviceEnvironmentApi'); - cy.getByTestSubj('serviceEnviromentComboBox') - .click({ force: true }) - .type('prod') - .type('{enter}'); - cy.contains('production').realClick(); + cy.getByTestSubj('serviceEnviromentComboBox').find('input').click(); + cy.getByTestSubj('comboBoxOptionsList serviceEnviromentComboBox-optionsList').should( + 'be.visible' + ); + cy.getByTestSubj('comboBoxOptionsList serviceEnviromentComboBox-optionsList') + .contains('button', 'production') + .click(); + cy.contains('Next step').click(); cy.contains('Create configuration'); cy.contains('Edit').click(); @@ -152,13 +154,23 @@ describe('Agent configuration', () => { 'serviceEnvironmentApi' ); cy.contains('Create configuration').click(); - cy.getByTestSubj('serviceNameComboBox').click().type('All').type('{enter}'); - cy.contains('All').realClick(); + cy.getByTestSubj('serviceNameComboBox').find('input').type('All{enter}'); + cy.getByTestSubj('serviceNameComboBox').find('input').click(); + cy.getByTestSubj('comboBoxOptionsList serviceNameComboBox-optionsList').should('be.visible'); + + cy.getByTestSubj('comboBoxOptionsList serviceNameComboBox-optionsList') + .contains('button', 'All') + .click(); cy.wait('@serviceEnvironmentApi'); - cy.getByTestSubj('serviceEnviromentComboBox').click({ force: true }).type('All'); + cy.getByTestSubj('serviceEnviromentComboBox').find('input').click(); + cy.getByTestSubj('comboBoxOptionsList serviceEnviromentComboBox-optionsList').should( + 'be.visible' + ); + cy.getByTestSubj('comboBoxOptionsList serviceEnviromentComboBox-optionsList') + .contains('button', 'All') + .click(); - cy.get('mark').contains('All').click({ force: true }); cy.contains('Next step').click(); cy.get('[data-test-subj="settingsPage_serviceName"]').contains('All'); cy.get('[data-test-subj="settingsPage_environmentName"]').contains('All'); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx index 5432c70e57ab1..4b93123b589bf 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { ReactNode } from 'react'; +import React, { PropsWithChildren } from 'react'; import { merge } from 'lodash'; import { createMemoryHistory } from 'history'; -import { renderHook, act } from '@testing-library/react-hooks'; + +import { act, waitFor, renderHook } from '@testing-library/react'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -22,7 +23,7 @@ import { fromQuery } from '../../shared/links/url_helpers'; import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; import type { APIEndpoint } from '../../../../server'; -function wrapper({ children, error = false }: { children?: ReactNode; error: boolean }) { +function wrapper({ children, error = false }: PropsWithChildren<{ error?: boolean }>) { const getHttpMethodMock = (method: 'GET' | 'POST') => jest.fn().mockImplementation(async (pathname) => { await delay(100); @@ -107,17 +108,18 @@ describe('useFailedTransactionsCorrelations', () => { wrapper, }); - try { + await waitFor(() => expect(result.current.progress).toEqual({ isRunning: true, loaded: 0, - }); - expect(result.current.response).toEqual({ ccsWarning: false }); - expect(typeof result.current.startFetch).toEqual('function'); - expect(typeof result.current.cancelFetch).toEqual('function'); - } finally { - unmount(); - } + }) + ); + + expect(result.current.response).toEqual({ ccsWarning: false }); + expect(result.current.startFetch).toEqual(expect.any(Function)); + expect(result.current.cancelFetch).toEqual(expect.any(Function)); + + unmount(); }); it('should not have received any results after 50ms', async () => { @@ -125,21 +127,21 @@ describe('useFailedTransactionsCorrelations', () => { wrapper, }); - try { - jest.advanceTimersByTime(50); + jest.advanceTimersByTime(50); + await waitFor(() => expect(result.current.progress).toEqual({ isRunning: true, loaded: 0, - }); - expect(result.current.response).toEqual({ ccsWarning: false }); - } finally { - unmount(); - } + }) + ); + + expect(result.current.response).toEqual({ ccsWarning: false }); + unmount(); }); it('should receive partial updates and finish running', async () => { - const { result, unmount, waitFor } = renderHook(() => useFailedTransactionsCorrelations(), { + const { result, unmount } = renderHook(() => useFailedTransactionsCorrelations(), { wrapper, }); @@ -253,29 +255,37 @@ describe('useFailedTransactionsCorrelations', () => { }); describe('when throwing an error', () => { it('should automatically start fetching results', async () => { - const { result, unmount } = renderHook(() => useFailedTransactionsCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useFailedTransactionsCorrelations, { + wrapper: ({ children }) => + React.createElement( + wrapper, + { + error: true, + }, + children + ), }); - try { + await waitFor(() => expect(result.current.progress).toEqual({ isRunning: true, loaded: 0, - }); - } finally { - unmount(); - } + }) + ); + + unmount(); }); it('should still be running after 50ms', async () => { - const { result, unmount } = renderHook(() => useFailedTransactionsCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useFailedTransactionsCorrelations, { + wrapper: ({ children }) => + React.createElement( + wrapper, + { + error: true, + }, + children + ), }); try { @@ -292,11 +302,15 @@ describe('useFailedTransactionsCorrelations', () => { }); it('should stop and return an error after more than 100ms', async () => { - const { result, unmount, waitFor } = renderHook(() => useFailedTransactionsCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useFailedTransactionsCorrelations, { + wrapper: ({ children }) => + React.createElement( + wrapper, + { + error: true, + }, + children + ), }); try { @@ -316,7 +330,7 @@ describe('useFailedTransactionsCorrelations', () => { describe('when canceled', () => { it('should stop running', async () => { - const { result, unmount, waitFor } = renderHook(() => useFailedTransactionsCorrelations(), { + const { result, unmount } = renderHook(() => useFailedTransactionsCorrelations(), { wrapper, }); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_latency_correlations.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_latency_correlations.test.tsx index 70446d1d2b1fd..e76420e3c1a92 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_latency_correlations.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/correlations/use_latency_correlations.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { ReactNode } from 'react'; +import React, { PropsWithChildren } from 'react'; import { merge } from 'lodash'; import { createMemoryHistory } from 'history'; -import { renderHook, act } from '@testing-library/react-hooks'; + +import { act, waitFor, renderHook } from '@testing-library/react'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -22,7 +23,7 @@ import { fromQuery } from '../../shared/links/url_helpers'; import { useLatencyCorrelations } from './use_latency_correlations'; import type { APIEndpoint } from '../../../../server'; -function wrapper({ children, error = false }: { children?: ReactNode; error: boolean }) { +function wrapper({ children, error = false }: PropsWithChildren<{ error?: boolean }>) { const getHttpMethodMock = (method: 'GET' | 'POST') => jest.fn().mockImplementation(async (pathname) => { await delay(100); @@ -86,16 +87,17 @@ function wrapper({ children, error = false }: { children?: ReactNode; error: boo } describe('useLatencyCorrelations', () => { - beforeEach(async () => { - jest.useFakeTimers({ legacyFakeTimers: true }); - }); - afterEach(() => { - jest.useRealTimers(); - }); - describe('when successfully loading results', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should automatically start fetching results', async () => { - const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + const { result, unmount } = renderHook(useLatencyCorrelations, { wrapper, }); @@ -113,7 +115,7 @@ describe('useLatencyCorrelations', () => { }); it('should not have received any results after 50ms', async () => { - const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + const { result, unmount } = renderHook(useLatencyCorrelations, { wrapper, }); @@ -131,7 +133,7 @@ describe('useLatencyCorrelations', () => { }); it('should receive partial updates and finish running', async () => { - const { result, unmount, waitFor } = renderHook(() => useLatencyCorrelations(), { + const { result, unmount } = renderHook(useLatencyCorrelations, { wrapper, }); @@ -213,12 +215,17 @@ describe('useLatencyCorrelations', () => { }); describe('when throwing an error', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should automatically start fetching results', async () => { - const { result, unmount } = renderHook(() => useLatencyCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useLatencyCorrelations, { + wrapper: ({ children }) => wrapper({ children, error: true }), }); try { @@ -232,11 +239,8 @@ describe('useLatencyCorrelations', () => { }); it('should still be running after 50ms', async () => { - const { result, unmount } = renderHook(() => useLatencyCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useLatencyCorrelations, { + wrapper: ({ children }) => wrapper({ children, error: true }), }); try { @@ -253,22 +257,21 @@ describe('useLatencyCorrelations', () => { }); it('should stop and return an error after more than 100ms', async () => { - const { result, unmount, waitFor } = renderHook(() => useLatencyCorrelations(), { - wrapper, - initialProps: { - error: true, - }, + const { result, unmount } = renderHook(useLatencyCorrelations, { + wrapper: ({ children }) => wrapper({ children, error: true }), }); try { - jest.advanceTimersByTime(150); - await waitFor(() => expect(result.current.progress.error).toBeDefined()); - - expect(result.current.progress).toEqual({ - error: 'Something went wrong', - isRunning: false, - loaded: 0, + act(() => { + jest.advanceTimersByTime(150); }); + await waitFor(() => + expect(result.current.progress).toEqual({ + error: 'Something went wrong', + isRunning: false, + loaded: 0, + }) + ); } finally { unmount(); } @@ -276,8 +279,16 @@ describe('useLatencyCorrelations', () => { }); describe('when canceled', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should stop running', async () => { - const { result, unmount, waitFor } = renderHook(() => useLatencyCorrelations(), { + const { result, unmount } = renderHook(useLatencyCorrelations, { wrapper, }); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/infra_overview/infra_tabs/use_tabs.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/infra_overview/infra_tabs/use_tabs.test.tsx index 245d4e90631a1..b1a1fea3924ee 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/infra_overview/infra_tabs/use_tabs.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/infra_overview/infra_tabs/use_tabs.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { ReactNode } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useTabs } from './use_tabs'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { CoreStart } from '@kbn/core/public'; diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/filter_warning.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/filter_warning.tsx new file mode 100644 index 0000000000000..71b487ff626c5 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/filter_warning.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + hostNames?: string[]; + containerIds?: string[]; +} + +export function FilterWarning({ containerIds = [], hostNames = [] }: Props) { + const hasContainerIds = containerIds.length > 0; + + return hasContainerIds ? ( + <FilterWarningToolTip + values={containerIds} + label={i18n.translate('xpack.apm.profiling.topFunctions.filteredLabel.containerId', { + defaultMessage: "Displaying profiling insights from the service's container id(s)", + })} + /> + ) : ( + <FilterWarningToolTip + values={hostNames} + label={i18n.translate('xpack.apm.profiling.topFunctions.filteredLabel.hostName', { + defaultMessage: "Displaying profiling insights from the service's host(s)", + })} + /> + ); +} + +interface FilterWarningToolTipProps { + values: string[]; + label: string; +} +function FilterWarningToolTip({ values = [], label }: FilterWarningToolTipProps) { + function renderTooltipOptions() { + return ( + <ul> + {values.map((value) => ( + <li key={value}>{`- ${value}`}</li> + ))} + </ul> + ); + } + + return ( + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem grow={false}> + <EuiText size="xs" color="subdued"> + {label} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip content={renderTooltipOptions()}> + <EuiIcon type="questionInCircle" /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/host_names_filter_warning.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/host_names_filter_warning.tsx deleted file mode 100644 index 0f3fb6cdb69fb..0000000000000 --- a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/host_names_filter_warning.tsx +++ /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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface Props { - hostNames?: string[]; -} -export function HostnamesFilterWarning({ hostNames = [] }: Props) { - function renderTooltipOptions() { - return ( - <ul> - {hostNames.map((hostName) => ( - <li key={hostName}>{`- ${hostName}`}</li> - ))} - </ul> - ); - } - - return ( - <EuiFlexGroup gutterSize="none"> - <EuiFlexItem grow={false}> - <EuiText size="xs" color="subdued"> - {i18n.translate('xpack.apm.profiling.flamegraph.filteredLabel', { - defaultMessage: "Displaying profiling insights from the service's host(s)", - })} - </EuiText> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip content={renderTooltipOptions()}> - <EuiIcon type="questionInCircle" /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - ); -} diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_flamegraph.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_flamegraph.tsx index 92442d223390f..0d6681d942d55 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_flamegraph.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_flamegraph.tsx @@ -9,12 +9,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { ApmDataSourceWithSummary } from '../../../../common/data_source'; import { ApmDocumentType } from '../../../../common/document_type'; -import { HOST_NAME } from '../../../../common/es_fields/apm'; +import { CONTAINER_ID, HOST_NAME } from '../../../../common/es_fields/apm'; import { mergeKueries, toKueryFilterFormat } from '../../../../common/utils/kuery_utils'; import { useFetcher } from '../../../hooks/use_fetcher'; import { FlamegraphChart } from '../../shared/charts/flamegraph'; import { ProfilingFlamegraphLink } from '../../shared/profiling/flamegraph/flamegraph_link'; -import { HostnamesFilterWarning } from './host_names_filter_warning'; +import { FilterWarning } from './filter_warning'; interface Props { serviceName: string; @@ -60,17 +60,20 @@ export function ProfilingHostsFlamegraph({ [dataSource, serviceName, start, end, environment, kuery] ); - const hostNamesKueryFormat = toKueryFilterFormat(HOST_NAME, data?.hostNames || []); + const profilingKueryFilter = + data?.containerIds && data.containerIds.length > 0 + ? toKueryFilterFormat(CONTAINER_ID, data?.containerIds || []) + : toKueryFilterFormat(HOST_NAME, data?.hostNames || []); return ( <> <EuiFlexGroup> <EuiFlexItem grow={false}> - <HostnamesFilterWarning hostNames={data?.hostNames} /> + <FilterWarning containerIds={data?.containerIds} hostNames={data?.hostNames} /> </EuiFlexItem> <EuiFlexItem> <ProfilingFlamegraphLink - kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])} + kuery={mergeKueries([`(${profilingKueryFilter})`, kuery])} rangeFrom={rangeFrom} rangeTo={rangeTo} justifyContent="flexEnd" diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_top_functions.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_top_functions.tsx index 88726f880b667..2ad1106ab9ad0 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_top_functions.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/profiling_overview/profiling_hosts_top_functions.tsx @@ -10,11 +10,11 @@ import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public'; import React from 'react'; import { ApmDataSourceWithSummary } from '../../../../common/data_source'; import { ApmDocumentType } from '../../../../common/document_type'; -import { HOST_NAME } from '../../../../common/es_fields/apm'; +import { CONTAINER_ID, HOST_NAME } from '../../../../common/es_fields/apm'; import { mergeKueries, toKueryFilterFormat } from '../../../../common/utils/kuery_utils'; import { isPending, useFetcher } from '../../../hooks/use_fetcher'; import { ProfilingTopNFunctionsLink } from '../../shared/profiling/top_functions/top_functions_link'; -import { HostnamesFilterWarning } from './host_names_filter_warning'; +import { FilterWarning } from './filter_warning'; interface Props { serviceName: string; @@ -66,17 +66,20 @@ export function ProfilingHostsTopNFunctions({ [dataSource, serviceName, start, end, environment, startIndex, endIndex, kuery] ); - const hostNamesKueryFormat = toKueryFilterFormat(HOST_NAME, data?.hostNames || []); + const profilingKueryFilter = + data?.containerIds && data.containerIds.length > 0 + ? toKueryFilterFormat(CONTAINER_ID, data?.containerIds || []) + : toKueryFilterFormat(HOST_NAME, data?.hostNames || []); return ( <> <EuiFlexGroup> <EuiFlexItem grow={false}> - <HostnamesFilterWarning hostNames={data?.hostNames} /> + <FilterWarning containerIds={data?.containerIds} hostNames={data?.hostNames} /> </EuiFlexItem> <EuiFlexItem> <ProfilingTopNFunctionsLink - kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])} + kuery={mergeKueries([`(${profilingKueryFilter})`, kuery])} rangeFrom={rangeFrom} rangeTo={rangeTo} justifyContent="flexEnd" diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/empty_banner.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/empty_banner.test.tsx index 20149d0f40795..168180e89c97e 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/empty_banner.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/empty_banner.test.tsx @@ -58,11 +58,12 @@ describe('EmptyBanner', () => { it('does not render null', async () => { const component = renderWithTheme(<EmptyBanner />, { wrapper }); - await act(async () => { + act(() => { cy.add({ data: { id: 'test id' } }); - await waitFor(() => { - expect(component.container.children.length).toBeGreaterThan(0); - }); + }); + + await waitFor(() => { + expect(component.container.children.length).toBeGreaterThan(0); }); }); }); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.test.tsx index 0f7fd67ae7c0f..31604d8934019 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import cytoscape from 'cytoscape'; import dagre from 'cytoscape-dagre'; import { EuiTheme } from '@kbn/kibana-react-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/trace_link.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/trace_link.test.tsx index 3250702b0cb80..06f7101520bab 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/trace_link.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/trace_link.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, render, waitFor } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { shallow } from 'enzyme'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; @@ -67,12 +67,9 @@ describe('TraceLink', () => { }, }); - let result; - act(() => { - const component = render(<TraceLink />, renderOptions); + const component = render(<TraceLink />, renderOptions); - result = component.getByText('Fetching trace...'); - }); + const result = component.getByText('Fetching trace...'); await waitFor(() => {}); expect(result).toBeDefined(); }); diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx index 05c8464929d55..fee28395960c9 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx @@ -6,7 +6,7 @@ */ import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; import { ServerlessType } from '../../../../../common/serverless'; diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_error_rate_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_error_rate_chart.tsx index f75f9f08cc31c..6cbd3e188720b 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_error_rate_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_error_rate_chart.tsx @@ -14,7 +14,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; -import { asInteger } from '../../../../../common/utils/formatters'; +import { asDecimalOrInteger } from '../../../../../common/utils/formatters'; import { TooltipContent } from './tooltip_content'; import { Popover } from './popover'; import { mergeKueries, toKueryFilterFormat } from '../../../../../common/utils/kuery_utils'; @@ -146,7 +146,7 @@ export function LogErrorRateChart({ height }: { height: number }) { showAnnotations={false} fetchStatus={status} timeseries={timeseries} - yLabelFormat={asInteger} + yLabelFormat={asDecimalOrInteger} /> </EuiPanel> ); diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_rate_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_rate_chart.tsx index f4d5981da38ef..8025a628067e7 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_rate_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/log_rates/log_rate_chart.tsx @@ -13,7 +13,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; -import { asInteger } from '../../../../../common/utils/formatters'; +import { asDecimalOrInteger } from '../../../../../common/utils/formatters'; import { TooltipContent } from './tooltip_content'; import { Popover } from './popover'; import { ChartType, getTimeSeriesColor } from '../helper/get_timeseries_color'; @@ -129,7 +129,7 @@ export function LogRateChart({ height }: { height: number }) { showAnnotations={false} fetchStatus={status} timeseries={timeseries} - yLabelFormat={asInteger} + yLabelFormat={asDecimalOrInteger} /> </EuiPanel> ); diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx index d71562e425e99..748b783661743 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeline/marker/error_marker.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { fireEvent } from '@testing-library/react'; -import { act } from '@testing-library/react-hooks'; +import { fireEvent, act } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx index 54999ead161ba..ce2a56a3ecc81 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/service_transactions_overview_link.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; +import { render, renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -19,7 +18,7 @@ import { const history = createMemoryHistory(); function wrapper({ queryParams }: { queryParams?: Record<string, unknown> }) { - return ({ children }: { children: React.ReactElement }) => ( + return ({ children }: React.PropsWithChildren) => ( <MockApmPluginContextWrapper history={history}> <MockUrlParamsContextProvider params={queryParams}>{children}</MockUrlParamsContextProvider> </MockApmPluginContextWrapper> diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx index bf095a4925fc9..a1836a2a336e0 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/links/apm/transaction_overview_link.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; +import { render, renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -15,7 +14,7 @@ import { TransactionOverviewLink, useTransactionsOverviewHref } from './transact const history = createMemoryHistory(); -function Wrapper({ children }: { children: React.ReactElement }) { +function Wrapper({ children }: React.PropsWithChildren) { return ( <MockApmPluginContextWrapper history={history}> <MockUrlParamsContextProvider>{children}</MockUrlParamsContextProvider> diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_breakpoints.test.tsx b/x-pack/plugins/observability_solution/apm/public/hooks/use_breakpoints.test.tsx index d311f48fa0e58..7a9a7aa33b6b2 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_breakpoints.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_breakpoints.test.tsx @@ -6,7 +6,7 @@ */ import React, { FC, PropsWithChildren } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { EuiProvider } from '@elastic/eui'; import { useBreakpoints } from './use_breakpoints'; diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_debounce.test.tsx b/x-pack/plugins/observability_solution/apm/public/hooks/use_debounce.test.tsx index 6701024eea9e9..98543601cab2a 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_debounce.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_debounce.test.tsx @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; import { useStateDebounced } from './use_debounce'; // Replace 'your-module' with the actual module path describe('useStateDebounced', () => { - jest.useFakeTimers(); beforeAll(() => { // Mocks console.error so it won't polute tests output when testing the api throwing error jest.spyOn(console, 'error').mockImplementation(() => null); @@ -18,6 +18,14 @@ describe('useStateDebounced', () => { jest.restoreAllMocks(); }); + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('returns the initial value and a debounced setter function', () => { const { result } = renderHook(() => useStateDebounced('initialValue', 300)); @@ -34,7 +42,9 @@ describe('useStateDebounced', () => { result.current[1]('updatedValue'); }); expect(result.current[0]).toBe('initialValue'); - jest.advanceTimersByTime(300); + act(() => { + jest.advanceTimersByTime(300); + }); expect(result.current[0]).toBe('updatedValue'); }); @@ -44,12 +54,15 @@ describe('useStateDebounced', () => { act(() => { result.current[1]('updatedValue'); }); - jest.advanceTimersByTime(150); + act(() => { + jest.advanceTimersByTime(150); + }); expect(result.current[0]).toBe('initialValue'); act(() => { result.current[1]('newUpdatedValue'); + jest.advanceTimersByTime(400); }); - jest.advanceTimersByTime(400); + expect(result.current[0]).toBe('newUpdatedValue'); }); }); diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_fetcher.test.tsx b/x-pack/plugins/observability_solution/apm/public/hooks/use_fetcher.test.tsx index 14c58ab3977ee..be61a03e2bd80 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_fetcher.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_fetcher.test.tsx @@ -5,46 +5,42 @@ * 2.0. */ -import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; -import React, { ReactNode } from 'react'; +import React from 'react'; +import { waitFor, act, renderHook, type RenderHookResult } from '@testing-library/react'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { delay } from '../utils/test_helpers'; -import { FetcherResult, useFetcher, isPending, FETCH_STATUS } from './use_fetcher'; +import { useFetcher, isPending, FETCH_STATUS } from './use_fetcher'; // Wrap the hook with a provider so it can useKibana const KibanaReactContext = createKibanaReactContext({ notifications: { toasts: { add: () => {}, danger: () => {} } }, } as unknown as Partial<CoreStart>); -interface WrapperProps { - children?: ReactNode; - callback: () => Promise<string>; - args: string[]; -} -function wrapper({ children }: WrapperProps) { +function wrapper({ children }: React.PropsWithChildren) { return <KibanaReactContext.Provider>{children}</KibanaReactContext.Provider>; } describe('useFetcher', () => { describe('when resolving after 500ms', () => { - let hook: RenderHookResult< - WrapperProps, - FetcherResult<string> & { - refetch: () => void; - } - >; + let hook: RenderHookResult<ReturnType<typeof useFetcher>, Parameters<typeof useFetcher>>; beforeEach(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); + async function fn() { await delay(500); return 'response from hook'; } - hook = renderHook(() => useFetcher(() => fn(), []), { wrapper }); + + hook = renderHook(() => useFetcher(fn, []), { wrapper }); }); - it('should have loading spinner initally', async () => { + afterEach(() => { + jest.useRealTimers(); + }); + + it('should have loading spinner initially', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, @@ -53,8 +49,10 @@ describe('useFetcher', () => { }); }); - it('should still show loading spinner after 100ms', async () => { - jest.advanceTimersByTime(100); + it('should still show loading spinner after 100ms', () => { + act(() => { + jest.advanceTimersByTime(100); + }); expect(hook.result.current).toEqual({ data: undefined, @@ -65,8 +63,11 @@ describe('useFetcher', () => { }); it('should show success after 1 second', async () => { - jest.advanceTimersByTime(1000); - await hook.waitForNextUpdate(); + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitFor(() => expect(hook.result.current.status).toBe('success')); expect(hook.result.current).toEqual({ data: 'response from hook', @@ -78,23 +79,23 @@ describe('useFetcher', () => { }); describe('when throwing after 500ms', () => { - let hook: RenderHookResult< - WrapperProps, - FetcherResult<string> & { - refetch: () => void; - } - >; + let hook: RenderHookResult<ReturnType<typeof useFetcher>, Parameters<typeof useFetcher>>; beforeEach(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); + async function fn(): Promise<string> { await delay(500); throw new Error('Something went wrong'); } - hook = renderHook(() => useFetcher(() => fn(), []), { wrapper }); + hook = renderHook(() => useFetcher(fn, []), { wrapper }); + }); + + afterEach(() => { + jest.useRealTimers(); }); - it('should have loading spinner initally', async () => { + it('should have loading spinner initially', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, @@ -103,8 +104,10 @@ describe('useFetcher', () => { }); }); - it('should still show loading spinner after 100ms', async () => { - jest.advanceTimersByTime(100); + it('should still show loading spinner after 100ms', () => { + act(() => { + jest.advanceTimersByTime(100); + }); expect(hook.result.current).toEqual({ data: undefined, @@ -115,8 +118,11 @@ describe('useFetcher', () => { }); it('should show error after 1 second', async () => { - jest.advanceTimersByTime(1000); - await hook.waitForNextUpdate(); + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitFor(() => expect(hook.result.current.status).toBe('failure')); expect(hook.result.current).toEqual({ data: undefined, @@ -128,20 +134,27 @@ describe('useFetcher', () => { }); describe('when a hook already has data', () => { - it('should show "first response" while loading "second response"', async () => { - jest.useFakeTimers({ legacyFakeTimers: true }); + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should show "first response" while loading "second response"', async () => { const hook = renderHook( /* eslint-disable-next-line react-hooks/exhaustive-deps */ ({ callback, args }) => useFetcher(callback, args), { initialProps: { - callback: async () => 'first response', + callback: () => Promise.resolve('first response'), args: ['a'], }, wrapper, } ); + expect(hook.result.current).toEqual({ data: undefined, error: undefined, @@ -149,15 +162,15 @@ describe('useFetcher', () => { status: 'loading', }); - await hook.waitForNextUpdate(); - // assert: first response has loaded and should be rendered - expect(hook.result.current).toEqual({ - data: 'first response', - error: undefined, - refetch: expect.any(Function), - status: 'success', - }); + await waitFor(() => + expect(hook.result.current).toEqual({ + data: 'first response', + error: undefined, + refetch: expect.any(Function), + status: 'success', + }) + ); // act: re-render hook with async callback hook.rerender({ @@ -168,55 +181,92 @@ describe('useFetcher', () => { args: ['b'], }); - jest.advanceTimersByTime(100); - - // assert: while loading new data the previous data should still be rendered - expect(hook.result.current).toEqual({ - data: 'first response', - error: undefined, - refetch: expect.any(Function), - status: 'loading', + act(() => { + jest.advanceTimersByTime(100); }); - jest.advanceTimersByTime(500); - await hook.waitForNextUpdate(); + // assert: while loading new data the previous data should still be rendered + await waitFor(() => + expect(hook.result.current).toEqual({ + data: 'first response', + error: undefined, + refetch: expect.any(Function), + status: 'loading', + }) + ); - // assert: "second response" has loaded and should be rendered - expect(hook.result.current).toEqual({ - data: 'second response', - error: undefined, - refetch: expect.any(Function), - status: 'success', + act(() => { + jest.advanceTimersByTime(500); }); + + await waitFor(() => + expect(hook.result.current).toEqual({ + data: 'second response', + error: undefined, + refetch: expect.any(Function), + status: 'success', + }) + ); }); it('should return the same object reference when data is unchanged between rerenders', async () => { + const initialProps = { + callback: () => Promise.resolve('data response'), + args: ['a'], + }; + const hook = renderHook( /* eslint-disable-next-line react-hooks/exhaustive-deps */ ({ callback, args }) => useFetcher(callback, args), { - initialProps: { - callback: async () => 'data response', - args: ['a'], - }, + initialProps, wrapper, } ); - await hook.waitForNextUpdate(); + + act(() => { + jest.runAllTimers(); + }); + + // assert: initial data has loaded; + await waitFor(() => + expect(hook.result.current).toEqual( + expect.objectContaining({ + data: 'data response', + status: 'success', + }) + ) + ); + const firstResult = hook.result.current; - hook.rerender(); + hook.rerender(initialProps); + + expect(hook.result.current).toEqual( + expect.objectContaining({ + data: 'data response', + status: 'success', + }) + ); + const secondResult = hook.result.current; // assert: subsequent rerender returns the same object reference expect(secondResult === firstResult).toEqual(true); hook.rerender({ - callback: async () => { - return 'second response'; - }, + callback: () => Promise.resolve('second response'), args: ['b'], }); - await hook.waitForNextUpdate(); + + await waitFor(() => + expect(hook.result.current).toEqual( + expect.objectContaining({ + data: 'second response', + status: 'success', + }) + ) + ); + const thirdResult = hook.result.current; // assert: rerender with different data returns a new object diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability_solution/apm/public/hooks/use_time_range.test.ts index 2bbc5c60ca91e..2f6c08aad8ccf 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_time_range.test.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; + +import { renderHook, RenderHookResult } from '@testing-library/react'; import { useTimeRange } from './use_time_range'; describe('useTimeRange', () => { - let hook: RenderHookResult<Parameters<typeof useTimeRange>[0], ReturnType<typeof useTimeRange>>; + let hook: RenderHookResult<ReturnType<typeof useTimeRange>, Parameters<typeof useTimeRange>[0]>; beforeEach(() => { Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 12)).valueOf()); diff --git a/x-pack/plugins/observability_solution/apm/scripts/test/dat.js b/x-pack/plugins/observability_solution/apm/scripts/test/dat.js new file mode 100644 index 0000000000000..b457cd4da5e76 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/scripts/test/dat.js @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 no-console */ +const { times } = require('lodash'); +const yargs = require('yargs'); +const path = require('path'); +const childProcess = require('child_process'); +const { REPO_ROOT } = require('@kbn/repo-info'); + +const { argv } = yargs(process.argv.slice(2)) + .option('serverless', { + default: false, + type: 'boolean', + description: 'Loads serverless configuration', + }) + .option('stateful', { + default: false, + type: 'boolean', + description: 'Loads stateful configuration', + }) + .option('server', { + default: false, + type: 'boolean', + description: 'Only start ES and Kibana', + }) + .option('runner', { + default: false, + type: 'boolean', + description: 'Only run tests', + }) + .option('grep', { + alias: 'spec', + type: 'string', + description: 'Specify the specs to run', + }) + .option('grep-files', { + alias: 'files', + type: 'array', + string: true, + description: 'Specify the files to run', + }) + .option('inspect', { + default: false, + type: 'boolean', + description: 'Add --inspect-brk flag to the ftr for debugging', + }) + .option('times', { + type: 'number', + description: 'Repeat the test n number of times', + }) + .option('updateSnapshots', { + default: false, + type: 'boolean', + description: 'Update snapshots', + }) + .option('bail', { + default: false, + type: 'boolean', + description: 'Stop the test run at the first failure', + }) + .check((argv) => { + const { inspect, runner } = argv; + if (inspect && !runner) { + throw new Error('--inspect can only be used with --runner'); + } else { + return true; + } + }) + .help(); + +const { serverless, stateful, bail, server, runner, grep, grepFiles, inspect, updateSnapshots } = + argv; + +if ((serverless === false && stateful === false) || (serverless && stateful)) { + throw new Error('Please specify either --stateful or --serverless'); +} + +let ftrScript = 'functional_tests'; +if (server) { + ftrScript = 'functional_tests_server'; +} else if (runner) { + ftrScript = 'functional_test_runner'; +} + +const environment = serverless ? 'serverless' : 'stateful'; + +const cmd = [ + 'node', + ...(inspect ? ['--inspect-brk'] : []), + `${REPO_ROOT}/scripts/${ftrScript}`, + ...(grep ? [`--grep "${grep}"`] : []), + ...(updateSnapshots ? [`--updateSnapshots`] : []), + ...(bail ? [`--bail`] : []), + `--config ${REPO_ROOT}/x-pack/test/api_integration/deployment_agnostic/configs/${environment}/oblt.apm.${environment}.config.ts`, +].join(' '); + +console.log(`Running: "${cmd}"`); + +function runTests() { + childProcess.execSync(cmd, { + cwd: path.join(__dirname), + stdio: 'inherit', + env: { ...process.env, APM_TEST_GREP_FILES: JSON.stringify(grepFiles) }, + }); +} + +if (argv.times) { + const runCounter = { succeeded: 0, failed: 0, remaining: argv.times }; + let exitStatus = 0; + times(argv.times, () => { + try { + runTests(); + runCounter.succeeded++; + } catch (e) { + if (bail) { + throw e; + } + + exitStatus = 1; + runCounter.failed++; + } + runCounter.remaining--; + if (argv.times > 1) { + console.log(runCounter); + } + }); + process.exit(exitStatus); +} else { + runTests(); +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts index 4792223610bb6..59c9cdac2df83 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts @@ -11,6 +11,7 @@ import { Logger, KibanaRequest, KibanaResponseFactory, RouteRegistrar } from '@k import { errors } from '@elastic/elasticsearch'; import agent from 'elastic-apm-node'; import { + DefaultRouteCreateOptions, IoTsParamsObject, ServerRouteRepository, stripNullishRequestParameters, @@ -30,6 +31,7 @@ import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; import { ApmFeatureFlags } from '../../../common/apm_feature_flags'; import type { APMCore, + APMRouteCreateOptions, MinimalApmPluginRequestHandlerContext, TelemetryUsageCounter, } from '../typings'; @@ -78,7 +80,11 @@ export function registerRoutes({ const router = core.setup.http.createRouter(); routes.forEach((route) => { - const { params, endpoint, options, handler } = route; + const { endpoint, handler, security } = route; + + const options = ('options' in route ? route.options : {}) as DefaultRouteCreateOptions & + APMRouteCreateOptions; + const params = 'params' in route ? route.params : undefined; const { method, pathname, version } = parseEndpoint(endpoint); @@ -215,6 +221,7 @@ export function registerRoutes({ path: pathname, options, validate: passThroughValidationObject, + security, }, wrappedHandler ); @@ -228,6 +235,7 @@ export function registerRoutes({ path: pathname, access: pathname.includes('/internal/apm') ? 'internal' : 'public', options, + security, }).addVersion( { version, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_host_names.ts b/x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_correlation_fields.ts similarity index 51% rename from x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_host_names.ts rename to x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_correlation_fields.ts index 11fe248da5632..2171b8e27de4a 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_host_names.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/profiling/get_service_correlation_fields.ts @@ -5,13 +5,26 @@ * 2.0. */ import { rangeQuery } from '@kbn/observability-plugin/server'; -import { ApmServiceTransactionDocumentType } from '../../../common/document_type'; -import { HOST_HOSTNAME, SERVICE_NAME } from '../../../common/es_fields/apm'; -import { RollupInterval } from '../../../common/rollup'; +import type { ApmServiceTransactionDocumentType } from '../../../common/document_type'; +import { + CONTAINER_ID, + HOST_HOSTNAME, + HOST_NAME, + SERVICE_NAME, +} from '../../../common/es_fields/apm'; +import type { RollupInterval } from '../../../common/rollup'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; +import type { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; -export async function getServiceHostNames({ +const getBucketKeysAsString = ( + buckets?: Array<{ + doc_count: number; + key: string | number; + key_as_string?: string | undefined; + }> +) => buckets?.map((bucket) => bucket.key as string) || []; + +export async function getServiceCorrelationFields({ apmEventClient, serviceName, start, @@ -45,15 +58,36 @@ export async function getServiceHostNames({ }, }, aggs: { - hostNames: { + hostHostNames: { terms: { field: HOST_HOSTNAME, size: 500, }, }, + hostNames: { + terms: { + field: HOST_NAME, + size: 500, + }, + }, + containerIds: { + terms: { + field: CONTAINER_ID, + size: 500, + }, + }, }, }, }); - return response.aggregations?.hostNames.buckets.map((bucket) => bucket.key as string) || []; + const allHostNames = [ + ...getBucketKeysAsString(response.aggregations?.hostHostNames.buckets), + ...getBucketKeysAsString(response.aggregations?.hostNames.buckets), + ]; + const hostNames = new Set<string>(allHostNames); + + return { + hostNames: Array.from(hostNames), + containerIds: getBucketKeysAsString(response.aggregations?.containerIds.buckets), + }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/profiling/hosts/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/profiling/hosts/route.ts index 5a748ddf5e11a..3ddd2782f1085 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/profiling/hosts/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/profiling/hosts/route.ts @@ -8,7 +8,7 @@ import { toNumberRt } from '@kbn/io-ts-utils'; import type { BaseFlameGraph, TopNFunctions } from '@kbn/profiling-utils'; import * as t from 'io-ts'; -import { HOST_NAME } from '../../../../common/es_fields/apm'; +import { CONTAINER_ID, HOST_NAME } from '../../../../common/es_fields/apm'; import { mergeKueries, toKueryFilterFormat } from '../../../../common/utils/kuery_utils'; import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; @@ -20,7 +20,7 @@ import { } from '../../default_api_types'; import { fetchFlamegraph } from '../fetch_flamegraph'; import { fetchFunctions } from '../fetch_functions'; -import { getServiceHostNames } from '../get_service_host_names'; +import { getServiceCorrelationFields } from '../get_service_correlation_fields'; const profilingHostsFlamegraphRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services/{serviceName}/profiling/hosts/flamegraph', @@ -31,7 +31,9 @@ const profilingHostsFlamegraphRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async ( resources - ): Promise<{ flamegraph: BaseFlameGraph; hostNames: string[] } | undefined> => { + ): Promise< + { flamegraph: BaseFlameGraph; hostNames: string[]; containerIds: string[] } | undefined + > => { const { context, plugins, params } = resources; const core = await context.core; const [esClient, apmEventClient, profilingDataAccessStart] = await Promise.all([ @@ -43,7 +45,7 @@ const profilingHostsFlamegraphRoute = createApmServerRoute({ const { start, end, environment, documentType, rollupInterval, kuery } = params.query; const { serviceName } = params.path; - const serviceHostNames = await getServiceHostNames({ + const { hostNames, containerIds } = await getServiceCorrelationFields({ apmEventClient, start, end, @@ -53,7 +55,7 @@ const profilingHostsFlamegraphRoute = createApmServerRoute({ rollupInterval, }); - if (!serviceHostNames.length) { + if (!hostNames.length && !containerIds.length) { return undefined; } const startSecs = start / 1000; @@ -65,10 +67,13 @@ const profilingHostsFlamegraphRoute = createApmServerRoute({ esClient: esClient.asCurrentUser, start: startSecs, end: endSecs, - kuery: mergeKueries([`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`, kuery]), + kuery: + containerIds.length > 0 + ? mergeKueries([`(${toKueryFilterFormat(CONTAINER_ID, containerIds)})`, kuery]) + : mergeKueries([`(${toKueryFilterFormat(HOST_NAME, hostNames)})`, kuery]), }); - return { flamegraph, hostNames: serviceHostNames }; + return { flamegraph, hostNames, containerIds }; } return undefined; @@ -90,7 +95,9 @@ const profilingHostsFunctionsRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async ( resources - ): Promise<{ functions: TopNFunctions; hostNames: string[] } | undefined> => { + ): Promise< + { functions: TopNFunctions; hostNames: string[]; containerIds: string[] } | undefined + > => { const { context, plugins, params } = resources; const core = await context.core; const [esClient, apmEventClient, profilingDataAccessStart] = await Promise.all([ @@ -103,7 +110,7 @@ const profilingHostsFunctionsRoute = createApmServerRoute({ params.query; const { serviceName } = params.path; - const serviceHostNames = await getServiceHostNames({ + const { hostNames, containerIds } = await getServiceCorrelationFields({ apmEventClient, start, end, @@ -113,7 +120,7 @@ const profilingHostsFunctionsRoute = createApmServerRoute({ rollupInterval, }); - if (!serviceHostNames.length) { + if (!hostNames.length && !containerIds.length) { return undefined; } @@ -128,10 +135,13 @@ const profilingHostsFunctionsRoute = createApmServerRoute({ endIndex, start: startSecs, end: endSecs, - kuery: mergeKueries([`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`, kuery]), + kuery: + containerIds.length > 0 + ? mergeKueries([`(${toKueryFilterFormat(CONTAINER_ID, containerIds)})`, kuery]) + : mergeKueries([`(${toKueryFilterFormat(HOST_NAME, hostNames)})`, kuery]), }); - return { functions, hostNames: serviceHostNames }; + return { functions, hostNames, containerIds }; } return undefined; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/typings.ts b/x-pack/plugins/observability_solution/apm/server/routes/typings.ts index f9ea085a11e6b..126ae48937648 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/typings.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/typings.ts @@ -9,7 +9,6 @@ import type { CoreSetup, CustomRequestHandlerContext, CoreStart, - RouteConfigOptions, IScopedClusterClient, IUiSettingsClient, SavedObjectsClientContract, @@ -48,21 +47,18 @@ export type MinimalApmPluginRequestHandlerContext = Omit< }; export interface APMRouteCreateOptions { - options: { - tags: Array< - | 'access:apm' - | 'access:apm_write' - | 'access:apm_settings_write' - | 'access:ml:canGetJobs' - | 'access:ml:canCreateJob' - | 'access:ml:canCloseJob' - | 'access:ai_assistant' - | 'oas-tag:APM agent keys' - | 'oas-tag:APM annotations' - >; - body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; - disableTelemetry?: boolean; - } & RouteConfigOptions<any>; + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:apm_settings_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + | 'access:ml:canCloseJob' + | 'access:ai_assistant' + | 'oas-tag:APM agent keys' + | 'oas-tag:APM annotations' + >; + disableTelemetry?: boolean; } export type TelemetryUsageCounter = ReturnType<UsageCollectionSetup['createUsageCounter']>; diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts index 4e1970d7fc887..db5620e0778f0 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts @@ -39,14 +39,18 @@ export function registerRoutes({ const router = core.http.createRouter(); routes.forEach((route) => { - const { endpoint, options, handler, params } = route; + const { endpoint, handler } = route; const { pathname, method } = parseEndpoint(endpoint); + const params = 'params' in route ? route.params : undefined; + const options = 'options' in route ? route.options : {}; + (router[method] as RouteRegistrar<typeof method, DatasetQualityRequestHandlerContext>)( { path: pathname, validate: passThroughValidationObject, options, + security: route.security, }, async (context, request, response) => { try { diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/types.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/types.ts index 86dd9e0986257..298d0c45efc45 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/types.ts @@ -29,7 +29,5 @@ export interface DatasetQualityRouteHandlerResources { } export interface DatasetQualityRouteCreateOptions { - options: { - tags: string[]; - }; + tags: string[]; } diff --git a/x-pack/plugins/observability_solution/entity_manager_app/README.md b/x-pack/plugins/observability_solution/entity_manager_app/README.md new file mode 100644 index 0000000000000..1fd230a046c54 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/README.md @@ -0,0 +1,3 @@ +# Entity Manager App Plugin + +This plugin provides a user interface to interact with the Entity Manager. \ No newline at end of file diff --git a/x-pack/plugins/observability_solution/entity_manager_app/jest.config.js b/x-pack/plugins/observability_solution/entity_manager_app/jest.config.js new file mode 100644 index 0000000000000..d8217a43063a2 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/jest.config.js @@ -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. + */ + +const path = require('path'); + +module.exports = { + preset: '@kbn/test', + rootDir: path.resolve(__dirname, '../../../..'), + roots: ['<rootDir>/x-pack/plugins/observability_solution/entity_manager_app'], + coverageDirectory: + '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/observability_solution/entity_manager_app', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '<rootDir>/x-pack/plugins/observability_solution/entity_manager_app/{common,public,server}/**/*.{js,ts,tsx}', + ], +}; diff --git a/x-pack/plugins/observability_solution/entity_manager_app/kibana.jsonc b/x-pack/plugins/observability_solution/entity_manager_app/kibana.jsonc new file mode 100644 index 0000000000000..93e6687f9b4c3 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/kibana.jsonc @@ -0,0 +1,29 @@ +{ + "type": "plugin", + "id": "@kbn/entityManager-app-plugin", + "owner": "@elastic/obs-entities", + "group": "observability", + "visibility": "private", + "description": "Entity manager plugin for entity assets (inventory, topology, etc)", + "plugin": { + "id": "entityManagerApp", + "configPath": ["xpack", "entityManagerApp"], + "browser": true, + "server": false, + "requiredPlugins": [ + "entityManager", + "observabilityShared", + "presentationUtil", + "usageCollection", + "licensing" + ], + "optionalPlugins": [ + "cloud", + "serverless" + ], + "requiredBundles": [ + "kibanaReact", + "kibanaUtils" + ] + } +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/application.tsx b/x-pack/plugins/observability_solution/entity_manager_app/public/application.tsx new file mode 100644 index 0000000000000..8f2e9e2213ba0 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/application.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public'; +import { PerformanceContextProvider } from '@kbn/ebt-tools'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { EntityClient } from '@kbn/entityManager-plugin/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { Router } from '@kbn/shared-ux-router'; +import { PluginContext } from './context/plugin_context'; +import { EntityManagerPluginStart } from './types'; +import { EntityManagerOverviewPage } from './pages/overview'; + +export function renderApp({ + core, + plugins, + appMountParameters, + ObservabilityPageTemplate, + usageCollection, + isDev, + kibanaVersion, + isServerless, + entityClient, +}: { + core: CoreStart; + plugins: EntityManagerPluginStart; + appMountParameters: AppMountParameters; + ObservabilityPageTemplate: React.ComponentType<LazyObservabilityPageTemplateProps>; + usageCollection: UsageCollectionSetup; + isDev?: boolean; + kibanaVersion: string; + isServerless?: boolean; + entityClient: EntityClient; +}) { + const { element, history, theme$ } = appMountParameters; + const isDarkMode = core.theme.getTheme().darkMode; + + // ensure all divs are .kbnAppWrappers + element.classList.add(APP_WRAPPER_CLASS); + + const ApplicationUsageTrackingProvider = + usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + + const CloudProvider = plugins.cloud?.CloudContextProvider ?? React.Fragment; + + ReactDOM.render( + <KibanaRenderContextProvider {...core}> + <ApplicationUsageTrackingProvider> + <KibanaThemeProvider {...{ theme: { theme$ } }}> + <CloudProvider> + <KibanaContextProvider + services={{ + ...core, + ...plugins, + storage: new Storage(localStorage), + entityClient: new EntityClient(core), + isDev, + kibanaVersion, + isServerless, + }} + > + <PluginContext.Provider + value={{ + isDev, + isServerless, + appMountParameters, + ObservabilityPageTemplate, + entityClient, + }} + > + <Router history={history}> + <EuiThemeProvider darkMode={isDarkMode}> + <RedirectAppLinks coreStart={core} data-test-subj="observabilityMainContainer"> + <PerformanceContextProvider> + <EntityManagerOverviewPage /> + </PerformanceContextProvider> + </RedirectAppLinks> + </EuiThemeProvider> + </Router> + </PluginContext.Provider> + </KibanaContextProvider> + </CloudProvider> + </KibanaThemeProvider> + </ApplicationUsageTrackingProvider> + </KibanaRenderContextProvider>, + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/context/plugin_context.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/context/plugin_context.ts new file mode 100644 index 0000000000000..7da2833be4395 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/context/plugin_context.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext } from 'react'; +import type { AppMountParameters } from '@kbn/core/public'; +import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; +import { EntityClient } from '@kbn/entityManager-plugin/public'; + +export interface PluginContextValue { + isDev?: boolean; + isServerless?: boolean; + appMountParameters?: AppMountParameters; + ObservabilityPageTemplate: React.ComponentType<LazyObservabilityPageTemplateProps>; + entityClient: EntityClient; +} + +export const PluginContext = createContext<PluginContextValue | null>(null); diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_kibana.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_kibana.ts new file mode 100644 index 0000000000000..a515b9b80b014 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_kibana.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 { CoreStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { EntityClient } from '@kbn/entityManager-plugin/public'; + +export type StartServices<AdditionalServices extends object = {}> = CoreStart & + AdditionalServices & { + storage: Storage; + kibanaVersion: string; + entityClient: EntityClient; + }; +const useTypedKibana = <AdditionalServices extends object = {}>() => + useKibana<StartServices<AdditionalServices>>(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_plugin_context.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_plugin_context.ts new file mode 100644 index 0000000000000..d0640deb575b2 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/hooks/use_plugin_context.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 { useContext } from 'react'; +import { PluginContext } from '../context/plugin_context'; +import type { PluginContextValue } from '../context/plugin_context'; + +export function usePluginContext(): PluginContextValue { + const context = useContext(PluginContext); + if (!context) { + throw new Error('Plugin context value is missing!'); + } + + return context; +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/index.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/index.ts new file mode 100644 index 0000000000000..5b83ea1d297d3 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { Plugin } from './plugin'; + +export const plugin: PluginInitializer<{}, {}> = (context: PluginInitializerContext) => { + return new Plugin(context); +}; diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/pages/overview/index.tsx b/x-pack/plugins/observability_solution/entity_manager_app/public/pages/overview/index.tsx new file mode 100644 index 0000000000000..8c978db6f6751 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/pages/overview/index.tsx @@ -0,0 +1,347 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { v4 as uuid } from 'uuid'; +import { + EuiBasicTable, + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { EntityV2 } from '@kbn/entities-schema'; +import { usePluginContext } from '../../hooks/use_plugin_context'; + +function EntitySourceForm({ + source, + index, + onFieldChange, +}: { + source: any; + index: number; + onFieldChange: Function; +}) { + const onArrayFieldChange = + (field: Exclude<keyof EntitySource, 'id'>) => (e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value.trim(); + if (!value) { + onFieldChange(index, field, []); + } else { + onFieldChange(index, field, e.target.value.trim().split(',')); + } + }; + + return ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow label="Index patterns (comma-separated)"> + <EuiFieldText + data-test-subj="entityManagerFormIndexPatterns" + name="index_patterns" + defaultValue={source.index_patterns.join(',')} + isInvalid={source.index_patterns.length === 0} + onChange={onArrayFieldChange('index_patterns')} + /> + </EuiFormRow> + </EuiFlexItem> + + <EuiFlexItem> + <EuiFormRow label="Identify fields (comma-separated field names)"> + <EuiFieldText + data-test-subj="entityManagerFormIdentityFields" + name="identity_fields" + defaultValue={source.identity_fields.join(',')} + isInvalid={source.identity_fields.length === 0} + onChange={onArrayFieldChange('identity_fields')} + /> + </EuiFormRow> + </EuiFlexItem> + + <EuiFlexItem> + <EuiFormRow label="Filters (comma-separated ESQL filters)"> + <EuiFieldText + data-test-subj="entityManagerFormFilters" + name="filters" + defaultValue={source.filters.join(',')} + onChange={onArrayFieldChange('filters')} + /> + </EuiFormRow> + </EuiFlexItem> + + <EuiFlexItem> + <EuiFormRow label="Metadata (comma-separated field names)"> + <EuiFieldText + data-test-subj="entityManagerFormMetadata" + name="metadata" + defaultValue={source.metadata_fields.join(',')} + onChange={onArrayFieldChange('metadata_fields')} + /> + </EuiFormRow> + </EuiFlexItem> + + <EuiFlexItem> + <EuiFormRow label="Timestamp field"> + <EuiFieldText + data-test-subj="entityManagerFormTimestamp" + name="timestamp_field" + defaultValue={source.timestamp_field} + onChange={(e) => onFieldChange(index, 'timestamp_field', e.target.value)} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + ); +} + +interface EntitySource { + id: string; + index_patterns?: string[]; + identity_fields?: string[]; + metadata_fields?: string[]; + filters?: string[]; + timestamp_field?: string; +} + +const newEntitySource = ({ + indexPatterns = [], + identityFields = [], + metadataFields = [], + filters = [], + timestampField = '@timestamp', +}: { + indexPatterns?: string[]; + identityFields?: string[]; + metadataFields?: string[]; + filters?: string[]; + timestampField?: string; +}) => ({ + id: uuid(), + index_patterns: indexPatterns, + identity_fields: identityFields, + metadata_fields: metadataFields, + timestamp_field: timestampField, + filters, +}); + +export function EntityManagerOverviewPage() { + const { ObservabilityPageTemplate, entityClient } = usePluginContext(); + const [previewEntities, setPreviewEntities] = useState<EntityV2[]>([]); + const [isSearchingEntities, setIsSearchingEntities] = useState(false); + const [previewError, setPreviewError] = useState(null); + const [formErrors, setFormErrors] = useState<string[]>([]); + const [entityType, setEntityType] = useState('service'); + const [entitySources, setEntitySources] = useState([ + newEntitySource({ + indexPatterns: ['remote_cluster:logs-*'], + identityFields: ['service.name'], + }), + ]); + + const searchEntities = async () => { + if ( + !entitySources.some( + (source) => source.identity_fields.length > 0 && source.index_patterns.length > 0 + ) + ) { + setFormErrors(['No valid source found']); + return; + } + + setIsSearchingEntities(true); + setFormErrors([]); + setPreviewError(null); + + try { + const { entities } = await entityClient.repositoryClient( + 'POST /internal/entities/v2/_search/preview', + { + params: { + body: { + sources: entitySources + .filter( + (source) => source.index_patterns.length > 0 && source.identity_fields.length > 0 + ) + .map((source) => ({ ...source, type: entityType })), + }, + }, + } + ); + + setPreviewEntities(entities); + } catch (err) { + setPreviewError(err.body?.message); + } finally { + setIsSearchingEntities(false); + } + }; + + return ( + <ObservabilityPageTemplate + data-test-subj="entitiesPage" + pageHeader={{ + bottomBorder: true, + pageTitle: 'Entity Manager', + }} + > + <EuiForm component="form" isInvalid={formErrors.length > 0} error={formErrors}> + <EuiFlexGroup direction="column" gutterSize="s"> + <EuiFlexItem> + <EuiTitle size="s"> + <h2>Entity type</h2> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem> + <EuiFormRow> + <EuiFieldText + data-test-subj="entityManagerFormType" + name="type" + defaultValue={entityType} + placeholder="host, service, user..." + onChange={(e) => { + setEntityType(e.target.value.trim()); + }} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="m" /> + + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="s"> + <h2>Entity sources</h2> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiButtonIcon + data-test-subj="entityManagerFormAddSource" + iconType="plusInCircle" + onClick={() => setEntitySources([...entitySources, newEntitySource({})])} + /> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + {entitySources.map((source, i) => ( + <div key={source.id}> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="xxs"> + <h4>Source {i + 1}</h4> + </EuiTitle> + </EuiFlexItem> + {entitySources.length > 1 ? ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + data-test-subj="entityManagerFormRemoveSource" + color={'danger'} + iconType={'minusInCircle'} + onClick={() => { + entitySources.splice(i, 1); + setEntitySources(entitySources.map((_source) => ({ ..._source }))); + }} + /> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EntitySourceForm + source={source} + index={i} + onFieldChange={( + index: number, + field: Exclude<keyof EntitySource, 'id'>, + value: any + ) => { + entitySources[index][field] = value; + setEntitySources([...entitySources]); + }} + /> + {i === entitySources.length - 1 ? ( + <EuiSpacer size="m" /> + ) : ( + <EuiHorizontalRule margin="m" /> + )} + </div> + ))} + + <EuiFormRow> + <EuiFlexGroup> + <EuiFlexItem> + <EuiButton + data-test-subj="entityManagerFormPreview" + isDisabled={isSearchingEntities} + onClick={searchEntities} + > + Preview + </EuiButton> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton data-test-subj="entityManagerFormCreate" isDisabled={true} color="primary"> + Create + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiForm> + + <EuiSpacer size="s" /> + + {previewError ? ( + <EuiCallOut title="Error previewing entity definition" color="danger" iconType="error"> + <p>{previewError}</p> + </EuiCallOut> + ) : null} + + <EuiBasicTable + loading={isSearchingEntities} + tableCaption={'Preview entities'} + items={previewEntities} + columns={[ + { + field: 'entity.id', + name: 'entity.id', + }, + { + field: 'entity.type', + name: 'entity.type', + }, + { + field: 'entity.last_seen_timestamp', + name: 'entity.last_seen_timestamp', + }, + ...Array.from(new Set(entitySources.flatMap((source) => source.identity_fields))).map( + (field) => ({ + field, + name: field, + }) + ), + ...Array.from(new Set(entitySources.flatMap((source) => source.metadata_fields))).map( + (field) => ({ + field: `metadata.${field}`, + name: `metadata.${field}`, + }) + ), + ]} + /> + </ObservabilityPageTemplate> + ); +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/plugin.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/plugin.ts new file mode 100644 index 0000000000000..0db381522b020 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/plugin.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject } from 'rxjs'; +import { + App, + AppMountParameters, + AppStatus, + AppUpdater, + CoreSetup, + DEFAULT_APP_CATEGORIES, + PluginInitializerContext, +} from '@kbn/core/public'; +import { Logger } from '@kbn/logging'; +import { EntityClient } from '@kbn/entityManager-plugin/public'; + +import { + EntityManagerAppPluginClass, + EntityManagerPluginStart, + EntityManagerPluginSetup, +} from './types'; + +export class Plugin implements EntityManagerAppPluginClass { + public logger: Logger; + private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({})); + + constructor(private readonly context: PluginInitializerContext<{}>) { + this.logger = context.logger.get(); + } + + setup(core: CoreSetup<EntityManagerPluginStart, {}>, pluginSetup: EntityManagerPluginSetup) { + const kibanaVersion = this.context.env.packageInfo.version; + + const mount = async (params: AppMountParameters<unknown>) => { + const { renderApp } = await import('./application'); + const [coreStart, pluginsStart] = await core.getStartServices(); + + return renderApp({ + appMountParameters: params, + core: coreStart, + isDev: this.context.env.mode.dev, + kibanaVersion, + usageCollection: pluginSetup.usageCollection, + ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate, + plugins: pluginsStart, + isServerless: !!pluginsStart.serverless, + entityClient: new EntityClient(core), + }); + }; + + const appUpdater$ = this.appUpdater$; + const app: App = { + id: 'entity_manager', + title: 'Entity Manager', + order: 8002, + updater$: appUpdater$, + euiIconType: 'logoObservability', + appRoute: '/app/entity_manager', + category: DEFAULT_APP_CATEGORIES.observability, + mount, + visibleIn: [], + keywords: ['observability', 'monitor', 'entities'], + status: AppStatus.inaccessible, + }; + + core.application.register(app); + + return {}; + } + + start() { + return {}; + } + + stop() {} +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/routes.tsx b/x-pack/plugins/observability_solution/entity_manager_app/public/routes.tsx new file mode 100644 index 0000000000000..80baa45422bfd --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/routes.tsx @@ -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 React from 'react'; +import { EntityManagerOverviewPage } from './pages/overview'; + +interface RouteDef { + [key: string]: { + handler: () => React.ReactElement; + params: Record<string, string>; + exact: boolean; + }; +} + +export function getRoutes(): RouteDef { + return { + '/app/entity_manager': { + handler: () => <EntityManagerOverviewPage />, + params: {}, + exact: true, + }, + }; +} diff --git a/x-pack/plugins/observability_solution/entity_manager_app/public/types.ts b/x-pack/plugins/observability_solution/entity_manager_app/public/types.ts new file mode 100644 index 0000000000000..b735771d79f80 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/public/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Plugin as PluginClass } from '@kbn/core/public'; +import { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import { CloudStart } from '@kbn/cloud-plugin/public'; +import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { EntityManagerPublicPluginSetup } from '@kbn/entityManager-plugin/public/types'; + +export interface EntityManagerPluginSetup { + observabilityShared: ObservabilitySharedPluginSetup; + serverless?: ServerlessPluginSetup; + usageCollection: UsageCollectionSetup; + entityManager: EntityManagerPublicPluginSetup; +} + +export interface EntityManagerPluginStart { + presentationUtil: PresentationUtilPluginStart; + cloud?: CloudStart; + serverless?: ServerlessPluginStart; + observabilityShared: ObservabilitySharedPluginStart; +} + +export type EntityManagerAppPluginClass = PluginClass<{}, {}>; diff --git a/x-pack/plugins/observability_solution/entity_manager_app/tsconfig.json b/x-pack/plugins/observability_solution/entity_manager_app/tsconfig.json new file mode 100644 index 0000000000000..64c0a293a4e2e --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager_app/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../../typings/**/*", + "common/**/*", + "public/**/*", + "types/**/*" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/logging", + "@kbn/ebt-tools", + "@kbn/kibana-react-plugin", + "@kbn/kibana-utils-plugin", + "@kbn/observability-shared-plugin", + "@kbn/react-kibana-context-render", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-link-redirect-app", + "@kbn/usage-collection-plugin", + "@kbn/shared-ux-router", + "@kbn/presentation-util-plugin", + "@kbn/cloud-plugin", + "@kbn/serverless", + "@kbn/entityManager-plugin", + "@kbn/entities-schema", + ] +} diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx index ee95d2a7f61af..efb6623f80e33 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LensEmbeddableInput, TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { EmbedAction } from '../../header/embed_action'; import { AddToCaseAction } from '../../header/add_to_case_action'; import { useKibana } from '../../hooks/use_kibana'; @@ -94,7 +94,7 @@ export function ExpViewActionMenuContent({ {isSaveOpen && lensAttributes && ( <LensSaveModalComponent - initialInput={lensAttributes as unknown as LensEmbeddableInput} + initialInput={{ attributes: lensAttributes }} onClose={() => setIsSaveOpen(false)} // if we want to do anything after the viz is saved // right now there is no action, so an empty function diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 75a5c42c76444..846044a7a7b0a 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -22,6 +22,7 @@ import { obsvReportConfigMap } from '../obsv_exploratory_view'; import { sampleAttributeWithReferenceLines } from './test_data/sample_attribute_with_reference_lines'; import { lensPluginMock } from '@kbn/lens-plugin/public/mocks'; import { FormulaPublicApi, XYState } from '@kbn/lens-plugin/public'; +import { Query } from '@kbn/es-query'; describe('Lens Attribute', () => { mockAppDataView(); @@ -448,7 +449,9 @@ describe('Lens Attribute', () => { reportViewConfig.reportType, formulaHelper ).getJSON(); - expect(multiSeriesLensAttr.state.query.query).toEqual('transaction.duration.us < 60000000'); + expect((multiSeriesLensAttr.state.query as Query).query).toEqual( + 'transaction.duration.us < 60000000' + ); }); describe('Layer breakdowns', function () { diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts index dd98e9879e82a..282ae5b768267 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; -import { ExistsFilter, Filter, isExistsFilter } from '@kbn/es-query'; +import { type ExistsFilter, type Query, type Filter, isExistsFilter } from '@kbn/es-query'; import { AvgIndexPatternColumn, CardinalityIndexPatternColumn, @@ -1300,7 +1300,7 @@ export class LensAttributes { visualizationType: 'lnsXY' | 'lnsLegacyMetric' | 'lnsHeatmap' = 'lnsXY', lastRefresh?: number ): TypedLensByValueInput['attributes'] { - const query = this.globalFilter || this.layerConfigs[0].seriesConfig.query; + const query: Query | undefined = this.globalFilter || this.layerConfigs[0].seriesConfig.query; const { internalReferences, adHocDataViews } = this.getReferences(); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.test.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.test.ts index 392582d4f9eed..496029a8b436b 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { ContainerMetricTypes } from '../charts/types'; import { useK8sContainerPageViewMetricsCharts, @@ -48,10 +48,10 @@ describe('useDockerContainerCharts', () => { async (metric) => { const expectedOrder = getContainerChartsExpectedOrder(metric); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useDockerContainerPageViewMetricsCharts({ metricsDataViewId, metric }) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { charts } = result.current; @@ -68,13 +68,14 @@ describe('useDockerContainerCharts', () => { describe('useDockerKPIMetricsCharts', () => { it('should return an array of charts with correct order', async () => { const expectedOrder = ['cpuUsage', 'memoryUsage']; - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useDockerContainerKpiCharts({ dataViewId: metricsDataViewId }) ); - await waitForNextUpdate(); - expect(result.current).toHaveLength(expectedOrder.length); - result.current.forEach((chart, index) => { - expect(chart).toHaveProperty('id', expectedOrder[index]); + await waitFor(() => { + expect(result.current).toHaveLength(expectedOrder.length); + result.current.forEach((chart, index) => { + expect(chart).toHaveProperty('id', expectedOrder[index]); + }); }); }); }); @@ -86,10 +87,10 @@ describe('useK8sContainerCharts', () => { async (metric) => { const expectedOrder = getK8sContainerChartsExpectedOrder(metric); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useK8sContainerPageViewMetricsCharts({ metricsDataViewId, metric }) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { charts } = result.current; @@ -106,13 +107,14 @@ describe('useK8sContainerCharts', () => { describe('useK8sContainerKPIMetricsCharts', () => { it('should return an array of charts with correct order', async () => { const expectedOrder = ['k8sCpuUsage', 'k8sMemoryUsage']; - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useK8sContainerKpiCharts({ dataViewId: metricsDataViewId }) ); - await waitForNextUpdate(); - expect(result.current).toHaveLength(expectedOrder.length); - result.current.forEach((chart, index) => { - expect(chart).toHaveProperty('id', expectedOrder[index]); + await waitFor(() => { + expect(result.current).toHaveLength(expectedOrder.length); + result.current.forEach((chart, index) => { + expect(chart).toHaveProperty('id', expectedOrder[index]); + }); }); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts index e62defaac6d4b..500dd02c63e18 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts @@ -6,10 +6,16 @@ */ import * as z from '@kbn/zod'; -import { EntityDataStreamType, ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import { + EntityDataStreamType, + BUILT_IN_ENTITY_TYPES, +} from '@kbn/observability-shared-plugin/common'; import { useFetcher } from '../../../hooks/use_fetcher'; -const EntityTypeSchema = z.union([z.literal(ENTITY_TYPES.HOST), z.literal(ENTITY_TYPES.CONTAINER)]); +const EntityTypeSchema = z.union([ + z.literal(BUILT_IN_ENTITY_TYPES.HOST), + z.literal(BUILT_IN_ENTITY_TYPES.CONTAINER), +]); const EntityDataStreamSchema = z.union([ z.literal(EntityDataStreamType.METRICS), z.literal(EntityDataStreamType.LOGS), diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.test.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.test.ts index 006fae9bec753..f95ab156222eb 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { HostMetricTypes } from '../charts/types'; import { useHostKpiCharts, useHostCharts, useKubernetesCharts } from './use_host_metrics_charts'; @@ -40,10 +40,8 @@ describe('useHostCharts', () => { async (metric) => { const expectedOrder = getHostChartsExpectedOrder(metric, false); - const { result, waitForNextUpdate } = renderHook(() => - useHostCharts({ dataViewId, metric }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => useHostCharts({ dataViewId, metric })); + await waitFor(() => new Promise((resolve) => resolve(null))); const { charts } = result.current; @@ -60,10 +58,10 @@ describe('useHostCharts', () => { async (metric) => { const expectedOrder = getHostChartsExpectedOrder(metric, true); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useHostCharts({ dataViewId, metric, overview: true }) ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { charts } = result.current; @@ -80,10 +78,8 @@ describe('useHostCharts', () => { describe('useKubernetesCharts', () => { it('should return an array of charts with correct order - overview', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useKubernetesCharts({ dataViewId, overview: true }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => useKubernetesCharts({ dataViewId, overview: true })); + await waitFor(() => new Promise((resolve) => resolve(null))); const expectedOrder = ['nodeCpuCapacity', 'nodeMemoryCapacity']; @@ -97,8 +93,8 @@ describe('useKubernetesCharts', () => { }); it('should return an array of charts with correct order', async () => { - const { result, waitForNextUpdate } = renderHook(() => useKubernetesCharts({ dataViewId })); - await waitForNextUpdate(); + const { result } = renderHook(() => useKubernetesCharts({ dataViewId })); + await waitFor(() => new Promise((resolve) => resolve(null))); const expectedOrder = [ 'nodeCpuCapacity', @@ -119,8 +115,8 @@ describe('useKubernetesCharts', () => { describe('useHostKpiCharts', () => { it('should return an array of charts with correct order', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHostKpiCharts({ dataViewId })); - await waitForNextUpdate(); + const { result } = renderHook(() => useHostKpiCharts({ dataViewId })); + await waitFor(() => new Promise((resolve) => resolve(null))); const expectedOrder = ['cpuUsage', 'normalizedLoad1m', 'memoryUsage', 'diskUsage']; @@ -140,10 +136,8 @@ describe('useHostKpiCharts', () => { getSubtitle: () => 'Custom Subtitle', }; - const { result, waitForNextUpdate } = renderHook(() => - useHostKpiCharts({ dataViewId, ...options }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => useHostKpiCharts({ dataViewId, ...options })); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current).toHaveLength(4); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_loading_state.test.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_loading_state.test.ts index becd0e81c0a9a..2475c97cec0e8 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_loading_state.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_loading_state.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useLoadingState } from './use_loading_state'; import { useDatePickerContext, type UseDateRangeProviderProps } from './use_date_picker'; import { BehaviorSubject, EMPTY, of, Subject, Subscription, skip } from 'rxjs'; @@ -102,7 +102,7 @@ describe('useLoadingState', () => { }); it('should set isAutoRefreshRequestPending to true when there are requests pending', async () => { - const { result, unmount, waitFor } = renderHook(() => useLoadingState()); + const { result, unmount } = renderHook(() => useLoadingState()); let receivedValue = false; subscription.add( @@ -128,7 +128,7 @@ describe('useLoadingState', () => { }); it('should set isAutoRefreshRequestPending to false when all requests complete', async () => { - const { result, unmount, waitFor } = renderHook(() => useLoadingState()); + const { result, unmount } = renderHook(() => useLoadingState()); let receivedValue = true; subscription.add( @@ -153,7 +153,7 @@ describe('useLoadingState', () => { }); it('should not call updateSearchSessionId if waitUntilNextSessionCompletesMock$ returns empty', async () => { - const { unmount, waitFor } = renderHook(() => useLoadingState()); + const { unmount } = renderHook(() => useLoadingState()); // waitUntilNextSessionCompletes$ returns EMPTY when the status is loading or none sessionState$.next(SearchSessionState.Loading); @@ -171,7 +171,7 @@ describe('useLoadingState', () => { }); it('should call updateSearchSessionId when waitUntilNextSessionCompletesMock$ returns', async () => { - const { unmount, waitFor } = renderHook(() => useLoadingState()); + const { unmount } = renderHook(() => useLoadingState()); // waitUntilNextSessionCompletes$ returns something when the status is Completed or BackgroundCompleted sessionState$.next(SearchSessionState.Loading); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_profiling_kuery.test.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_profiling_kuery.test.tsx index 10325ce51f81f..0d4db96a23fad 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_profiling_kuery.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_profiling_kuery.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MemoryRouter, useHistory } from 'react-router-dom'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useProfilingKuery } from './use_profiling_kuery'; import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props'; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_request_observable.test.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_request_observable.test.ts index 9ca4bd17ca34b..9be24cbfcf3f4 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_request_observable.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_request_observable.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useRequestObservable } from './use_request_observable'; import { type RequestState, useLoadingStateContext } from './use_loading_state'; import { useDatePickerContext, type UseDateRangeProviderProps } from './use_date_picker'; @@ -69,7 +69,7 @@ describe('useRequestObservable', () => { }); it('should process a valid request function', async () => { - const { result, waitFor, unmount } = renderHook(() => useRequestObservable()); + const { result, unmount } = renderHook(() => useRequestObservable()); act(() => { result.current.request$.next(() => Promise.resolve()); @@ -85,7 +85,7 @@ describe('useRequestObservable', () => { }); it('should be able to make new requests if isAutoRefreshRequestPending is false', async () => { - const { result, waitFor, unmount } = renderHook(() => useRequestObservable()); + const { result, unmount } = renderHook(() => useRequestObservable()); act(() => { isAutoRefreshRequestPendingMock$.next(false); @@ -102,7 +102,7 @@ describe('useRequestObservable', () => { }); it('should block new requests when isAutoRefreshRequestPending is true', async () => { - const { result, waitFor, unmount } = renderHook(() => useRequestObservable()); + const { result, unmount } = renderHook(() => useRequestObservable()); act(() => { isAutoRefreshRequestPendingMock$.next(false); @@ -123,7 +123,7 @@ describe('useRequestObservable', () => { }); it('should not block new requests when auto-refresh is paused', async () => { - const { result, waitFor, unmount } = renderHook(() => useRequestObservable()); + const { result, unmount } = renderHook(() => useRequestObservable()); act(() => { autoRefreshConfig$.next({ isPaused: true, interval: 5000 }); @@ -144,7 +144,7 @@ describe('useRequestObservable', () => { }); it('should complete the request when an error is thrown', async () => { - const { result, waitFor, unmount } = renderHook(() => useRequestObservable()); + const { result, unmount } = renderHook(() => useRequestObservable()); act(() => { autoRefreshConfig$.next({ isPaused: true, interval: 5000 }); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx index 2177cd0509085..57a07d4dc296e 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { getFieldByType } from '@kbn/metrics-data-access-plugin/common'; import { decodeOrThrow } from '@kbn/io-ts-utils'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { useSourceContext } from '../../../../containers/metrics_source'; import { isPending, useFetcher } from '../../../../hooks/use_fetcher'; import { parseSearchString } from './parse_search_string'; @@ -58,7 +58,7 @@ export const Processes = () => { const { request$ } = useRequestObservable(); const { isActiveTab } = useTabSwitcherContext(); const { dataStreams, status: dataStreamsStatus } = useEntitySummary({ - entityType: ENTITY_TYPES.HOST, + entityType: BUILT_IN_ENTITY_TYPES.HOST, entityId: asset.name, }); const addMetricsCalloutId: AddMetricsCalloutKey = 'hostProcesses'; diff --git a/x-pack/plugins/observability_solution/infra/public/containers/ml/infra_ml_module_status.test.ts b/x-pack/plugins/observability_solution/infra/public/containers/ml/infra_ml_module_status.test.ts index 80d538ef1e50b..b754537107e46 100644 --- a/x-pack/plugins/observability_solution/infra/public/containers/ml/infra_ml_module_status.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/containers/ml/infra_ml_module_status.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useModuleStatus } from './infra_ml_module_status'; describe('useModuleStatus', () => { diff --git a/x-pack/plugins/observability_solution/infra/public/containers/plugin_config_context.test.tsx b/x-pack/plugins/observability_solution/infra/public/containers/plugin_config_context.test.tsx index 62c582a818240..a33c3432f06fa 100644 --- a/x-pack/plugins/observability_solution/infra/public/containers/plugin_config_context.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/containers/plugin_config_context.test.tsx @@ -6,15 +6,15 @@ */ import type { InfraConfig } from '../../common/plugin_config_types'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import React from 'react'; import { PluginConfigProvider, usePluginConfig } from './plugin_config_context'; describe('usePluginConfig()', () => { it('throws an error if the context value was not set before using the hook', () => { - const { result } = renderHook(() => usePluginConfig()); - - expect(result.error).not.toEqual(undefined); + expect(() => renderHook(() => usePluginConfig())).toThrow( + /PluginConfigContext value was not initialized./ + ); }); it('returns the plugin config what was set through the provider', () => { @@ -40,7 +40,6 @@ describe('usePluginConfig()', () => { }, }); - expect(result.error).toEqual(undefined); expect(result.current).toEqual(config); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_alerts_count.test.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_alerts_count.test.ts index 2b2dc755c94c1..87db1db65398a 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_alerts_count.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_alerts_count.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { ALERT_STATUS, ValidFeatureId } from '@kbn/rule-data-utils'; import { useAlertsCount } from './use_alerts_count'; @@ -66,12 +66,12 @@ describe('useAlertsCount', () => { it('should return the mocked data from API', async () => { mockedPostAPI.mockResolvedValue(mockedAlertsCountResponse); - const { result, waitForNextUpdate } = renderHook(() => useAlertsCount({ featureIds })); + const { result } = renderHook(() => useAlertsCount({ featureIds })); expect(result.current.loading).toBe(true); expect(result.current.alertsCount).toEqual(undefined); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { alertsCount, loading, error } = result.current; expect(alertsCount).toEqual(expectedResult); @@ -88,15 +88,13 @@ describe('useAlertsCount', () => { }; mockedPostAPI.mockResolvedValue(mockedAlertsCountResponse); - const { waitForNextUpdate } = renderHook(() => + renderHook(() => useAlertsCount({ featureIds, query, }) ); - await waitForNextUpdate(); - const body = JSON.stringify({ aggs: { count: { @@ -108,9 +106,11 @@ describe('useAlertsCount', () => { size: 0, }); - expect(mockedPostAPI).toHaveBeenCalledWith( - '/internal/rac/alerts/find', - expect.objectContaining({ body }) + await waitFor(() => + expect(mockedPostAPI).toHaveBeenCalledWith( + '/internal/rac/alerts/find', + expect.objectContaining({ body }) + ) ); }); @@ -118,10 +118,8 @@ describe('useAlertsCount', () => { const error = new Error('Fetch Alerts Count Failed'); mockedPostAPI.mockRejectedValueOnce(error); - const { result, waitForNextUpdate } = renderHook(() => useAlertsCount({ featureIds })); - - await waitForNextUpdate(); + const { result } = renderHook(() => useAlertsCount({ featureIds })); - expect(result.current.error?.message).toMatch(error.message); + await waitFor(() => expect(result.current.error?.message).toMatch(error.message)); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.test.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.test.ts index 0a619069c04a4..5e5f00fd66139 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.test.ts @@ -6,7 +6,7 @@ */ import 'jest-canvas-mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useLensAttributes } from './use_lens_attributes'; import { coreMock } from '@kbn/core/public/mocks'; import { type KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public'; @@ -72,15 +72,15 @@ describe('useLensAttributes hook', () => { }); it('should return the basic lens attributes', async () => { - const { waitForNextUpdate } = renderHook(() => useLensAttributes(params)); - await waitForNextUpdate(); - - expect(LensConfigBuilderMock.mock.instances[0].build).toHaveBeenCalledWith(params); + renderHook(() => useLensAttributes(params)); + await waitFor(() => + expect(LensConfigBuilderMock.mock.instances[0].build).toHaveBeenCalledWith(params) + ); }); it('should return extra actions', async () => { - const { result, waitForNextUpdate } = renderHook(() => useLensAttributes(params)); - await waitForNextUpdate(); + const { result } = renderHook(() => useLensAttributes(params)); + await waitFor(() => new Promise((resolve) => resolve(null))); const extraActions = result.current.getExtraActions({ timeRange: { diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.ts index 9e0f9071eb079..b1248a1f05e1e 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_lens_attributes.ts @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { Filter, Query, TimeRange } from '@kbn/es-query'; +import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import useAsync from 'react-use/lib/useAsync'; @@ -37,7 +37,13 @@ export const useLensAttributes = (params: UseLensAttributesParams) => { }, [params, dataViews, lens]); const injectFilters = useCallback( - ({ filters, query }: { filters: Filter[]; query: Query }): LensAttributes | null => { + ({ + filters, + query, + }: { + filters: Filter[]; + query: Query | AggregateQuery; + }): LensAttributes | null => { if (!attributes) { return null; } @@ -63,7 +69,7 @@ export const useLensAttributes = (params: UseLensAttributesParams) => { }: { timeRange: TimeRange; filters: Filter[]; - query: Query; + query: Query | AggregateQuery; searchSessionId?: string; }) => () => { @@ -94,7 +100,7 @@ export const useLensAttributes = (params: UseLensAttributesParams) => { }: { timeRange: TimeRange; filters?: Filter[]; - query?: Query; + query?: Query | AggregateQuery; searchSessionId?: string; }) => { const openInLens = getOpenInLensAction( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/use_host_ip_to_name.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/link_to/use_host_ip_to_name.test.ts index 150416a204b05..2a9d56a6f4dfa 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/use_host_ip_to_name.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/use_host_ip_to_name.test.ts @@ -6,7 +6,7 @@ */ import { useHostIpToName } from './use_host_ip_to_name'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; const renderUseHostIpToNameHook = () => renderHook((props) => useHostIpToName(props.ipAddress, props.indexPattern), { @@ -32,10 +32,10 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { describe('useHostIpToName Hook', () => { it('should basically work', async () => { mockedFetch.mockResolvedValue({ host: 'example-01' } as any); - const { result, waitForNextUpdate } = renderUseHostIpToNameHook(); + const { result } = renderUseHostIpToNameHook(); expect(result.current.name).toBe(null); expect(result.current.loading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current.name).toBe('example-01'); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(null); @@ -44,10 +44,10 @@ describe('useHostIpToName Hook', () => { it('should handle errors', async () => { const error = new Error('Host not found'); mockedFetch.mockRejectedValue(error); - const { result, waitForNextUpdate } = renderUseHostIpToNameHook(); + const { result } = renderUseHostIpToNameHook(); expect(result.current.name).toBe(null); expect(result.current.loading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current.name).toBe(null); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(error); @@ -56,16 +56,16 @@ describe('useHostIpToName Hook', () => { it('should reset errors', async () => { const error = new Error('Host not found'); mockedFetch.mockRejectedValue(error); - const { result, waitForNextUpdate, rerender } = renderUseHostIpToNameHook(); + const { result, rerender } = renderUseHostIpToNameHook(); expect(result.current.name).toBe(null); expect(result.current.loading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current.name).toBe(null); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(error); mockedFetch.mockResolvedValue({ host: 'example-01' } as any); rerender({ ipAddress: '192.168.1.2', indexPattern: 'metricbeat-*' }); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current.name).toBe('example-01'); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(null); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index a0ebb20184912..abae2d5cadbff 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -6,7 +6,7 @@ */ import { type HostNodeRow, useHostsTable } from './use_hosts_table'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import * as useUnifiedSearchHooks from './use_unified_search'; import * as useHostsViewHooks from './use_hosts_view'; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_metrics_charts.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_metrics_charts.test.ts index b76d5938f0cb5..9d68b3ff76e77 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_metrics_charts.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/hooks/use_metrics_charts.test.ts @@ -6,16 +6,14 @@ */ import type { LensSeriesLayer } from '@kbn/lens-embeddable-utils/config_builder'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { PAGE_SIZE_OPTIONS } from '../constants'; import { useMetricsCharts } from './use_metrics_charts'; describe('useMetricsCharts', () => { it('should return an array of charts with breakdown config', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useMetricsCharts({ dataViewId: 'dataViewId' }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => useMetricsCharts({ dataViewId: 'dataViewId' })); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(result.current).toHaveLength(11); @@ -29,10 +27,8 @@ describe('useMetricsCharts', () => { }); it('should return an array of charts with correct order', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useMetricsCharts({ dataViewId: 'dataViewId' }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => useMetricsCharts({ dataViewId: 'dataViewId' })); + await waitFor(() => new Promise((resolve) => resolve(null))); const expectedOrder = [ 'cpuUsage', diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts index 7fe7b8b3fe18c..533857130b114 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { DataView } from '@kbn/data-views-plugin/common'; import { useWaffleFilters, WaffleFiltersState } from './use_waffle_filters'; import { TIMESTAMP_FIELD } from '../../../../../common/constants'; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts index 50f4e95b58a37..757a0e955b4df 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useWaffleOptions, WaffleOptionsState } from './use_waffle_options'; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx index bcea796c3b00a..0e70071f199ff 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useMetricsExplorerState } from './use_metric_explorer_state'; import { MetricsExplorerOptionsContainer } from './use_metrics_explorer_options'; import React from 'react'; @@ -75,6 +75,10 @@ describe('useMetricsExplorerState', () => { delete STORE.MetricsExplorerTimeRange; }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should just work', async () => { mockedUseMetricsExplorerData.mockReturnValue({ isLoading: false, @@ -92,12 +96,14 @@ describe('useMetricsExplorerState', () => { describe('handleRefresh', () => { it('should trigger an addition request when handleRefresh is called', async () => { const { result } = renderUseMetricsExplorerStateHook(); - expect(result.all.length).toBe(2); - const numberOfHookCalls = result.all.length; + + const numberOfHookCalls = mockedUseMetricsExplorerData.mock.calls.length; + + expect(numberOfHookCalls).toEqual(2); act(() => { result.current.refresh(); }); - expect(result.all.length).toBe(numberOfHookCalls + 1); + expect(mockedUseMetricsExplorerData).toHaveBeenCalledTimes(numberOfHookCalls + 1); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index 202ae51990ad2..8cc6bff922d92 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -9,7 +9,7 @@ import React, { FC, PropsWithChildren } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { DataView } from '@kbn/data-views-plugin/common'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, act, renderHook } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { @@ -108,15 +108,14 @@ describe('useMetricsExplorerData Hook', () => { it('should just work', async () => { mockedFetch.mockResolvedValue(resp); - const { result, waitForNextUpdate } = renderUseMetricsExplorerDataHook(); + const { result } = renderUseMetricsExplorerDataHook(); expect(result.current.data).toBeUndefined(); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); const { series } = result.current.data!.pages[0]; expect(series).toBeDefined(); expect(series.length).toBe(3); @@ -124,12 +123,11 @@ describe('useMetricsExplorerData Hook', () => { it('should paginate', async () => { mockedFetch.mockResolvedValue(resp); - const { result, waitForNextUpdate } = renderUseMetricsExplorerDataHook(); + const { result } = renderUseMetricsExplorerDataHook(); expect(result.current.data).toBeUndefined(); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); const { series } = result.current.data!.pages[0]; expect(series).toBeDefined(); expect(series.length).toBe(3); @@ -137,41 +135,45 @@ describe('useMetricsExplorerData Hook', () => { pageInfo: { total: 10, afterKey: 'host-06' }, series: [createSeries('host-04'), createSeries('host-05'), createSeries('host-06')], } as any); - result.current.fetchNextPage(); - await waitForNextUpdate(); - expect(result.current.isLoading).toBe(false); - const { series: nextSeries } = result.current.data!.pages[1]; - expect(nextSeries).toBeDefined(); - expect(nextSeries.length).toBe(3); + await act(async () => { + await result.current.fetchNextPage(); + }); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + const { series: nextSeries } = result.current.data!.pages[1]; + expect(nextSeries).toBeDefined(); + expect(nextSeries.length).toBe(3); + }); }); it('should reset error upon recovery', async () => { const error = new Error('Network Error'); mockedFetch.mockRejectedValue(error); - const { result, waitForNextUpdate } = renderUseMetricsExplorerDataHook(); + const { result } = renderUseMetricsExplorerDataHook(); expect(result.current.data).toBeUndefined(); expect(result.current.error).toEqual(null); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data).toBeUndefined(); expect(result.current.error).toEqual(error); - expect(result.current.isLoading).toBe(false); mockedFetch.mockResolvedValue(resp as any); - result.current.refetch(); - await waitForNextUpdate(); - expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); - expect(result.current.error).toBe(null); + await act(async () => { + await result.current.refetch(); + }); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.data!.pages[0]).toEqual(resp); + expect(result.current.error).toBe(null); + }); }); it('should not paginate on option change', async () => { mockedFetch.mockResolvedValue(resp as any); - const { result, waitForNextUpdate, rerender } = renderUseMetricsExplorerDataHook(); + const { result, rerender } = renderUseMetricsExplorerDataHook(); expect(result.current.data).toBeUndefined(); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); const { series } = result.current.data!.pages[0]; expect(series).toBeDefined(); expect(series.length).toBe(3); @@ -187,19 +189,19 @@ describe('useMetricsExplorerData Hook', () => { timestamps, }); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); - expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.data!.pages[0]).toEqual(resp); + }); }); it('should not paginate on time change', async () => { mockedFetch.mockResolvedValue(resp as any); - const { result, waitForNextUpdate, rerender } = renderUseMetricsExplorerDataHook(); + const { result, rerender } = renderUseMetricsExplorerDataHook(); expect(result.current.data).toBeUndefined(); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); const { series } = result.current.data!.pages[0]; expect(series).toBeDefined(); expect(series.length).toBe(3); @@ -211,8 +213,9 @@ describe('useMetricsExplorerData Hook', () => { timestamps: { fromTimestamp: 1678378092225, toTimestamp: 1678381693477, interval: '>=10s' }, }); expect(result.current.isLoading).toBe(true); - await waitForNextUpdate(); - expect(result.current.data!.pages[0]).toEqual(resp); - expect(result.current.isLoading).toBe(false); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.data!.pages[0]).toEqual(resp); + }); }); }); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx index 795728fa8d8ef..f5c257e1f86ac 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useMetricsExplorerOptions, MetricsExplorerOptions, diff --git a/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_data_search_request.test.tsx b/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_data_search_request.test.tsx index 33369c1125e15..08907b1627086 100644 --- a/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_data_search_request.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_data_search_request.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import React from 'react'; import { firstValueFrom, Observable, of, Subject } from 'rxjs'; import type { ISearchGeneric, IKibanaSearchResponse } from '@kbn/search-types'; diff --git a/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx b/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx index a4b01be03d80b..24433f23bc677 100644 --- a/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { IKibanaSearchRequest } from '@kbn/search-types'; import { ParsedDataSearchRequestDescriptor, ParsedKibanaSearchResponse } from './types'; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index c109f53be1f11..c7669e6f9acdd 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -44,18 +44,25 @@ export async function getLatestEntity({ return undefined; } - const response = await inventoryEsClient.esql<{ - source_data_stream?: { type?: string | string[] }; - }>('get_latest_entities', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const response = await inventoryEsClient.esql< + { + 'source_data_stream.type'?: string | string; + }, + { transform: 'plain' } + >( + 'get_latest_entities', + { + query: `FROM ${ENTITIES_LATEST_ALIAS} | WHERE ${ENTITY_TYPE} == ? | WHERE ${hostOrContainerIdentityField} == ? | KEEP ${SOURCE_DATA_STREAM_TYPE} `, - params: [entityType, entityId], - }); + params: [entityType, entityId], + }, + { transform: 'plain' } + ); - return { sourceDataStreamType: response[0].source_data_stream?.type }; + return { sourceDataStreamType: response.hits[0]['source_data_stream.type'] }; } catch (e) { logger.error(e); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts index 46f2cecf45254..4056faba9427e 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { METRICS_APP_ID } from '@kbn/deeplinks-observability/constants'; import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { createObservabilityEsClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { getInfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getDataStreamTypes } from './get_data_stream_types'; @@ -24,8 +24,8 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => { validate: { params: schema.object({ entityType: schema.oneOf([ - schema.literal(ENTITY_TYPES.HOST), - schema.literal(ENTITY_TYPES.CONTAINER), + schema.literal(BUILT_IN_ENTITY_TYPES.HOST), + schema.literal(BUILT_IN_ENTITY_TYPES.CONTAINER), ]), entityId: schema.string(), }), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 4da8fd3103c41..239d441b5d4e6 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -45,7 +45,7 @@ export function EntityIcon({ entity }: EntityIconProps) { return <AgentIcon agentName={castArray(entity.agent?.name)[0]} role="presentation" />; } - if (entity.entityType.startsWith('kubernetes')) { + if (entity.entityType.startsWith('k8s')) { return <EuiIcon type="logoKubernetes" size="l" />; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index 233c1a1076b79..c5b35b521b3bc 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useDetailViewRedirect } from './use_detail_view_redirect'; import { useKibana } from './use_kibana'; import { CONTAINER_ID, - ENTITY_TYPES, + BUILT_IN_ENTITY_TYPES, HOST_NAME, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; @@ -134,15 +134,30 @@ describe('useDetailViewRedirect', () => { }); [ - [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], - [ENTITY_TYPES.KUBERNETES.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'], - [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs, 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs, 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.JOB.ecs, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.NODE.ecs, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.POD.ecs, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], - [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs, + 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + ], + [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs, + 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs, + 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs, + 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + ], + [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], + [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], + [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, + 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + ], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { const entity: InventoryEntity = { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 36fa622e74667..02a77b8f2afa1 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -6,7 +6,7 @@ */ import { ASSET_DETAILS_LOCATOR_ID, - ENTITY_TYPES, + BUILT_IN_ENTITY_TYPES, SERVICE_OVERVIEW_LOCATOR_ID, type AssetDetailsLocatorParams, type ServiceOverviewParams, @@ -20,15 +20,19 @@ import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record<string, string> = { - [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', - [ENTITY_TYPES.KUBERNETES.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview', - [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', - [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs]: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]: + 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]: + 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.SERVICE.ecs]: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs]: + 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', }; export const useDetailViewRedirect = () => { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_is_loading_complete.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_is_loading_complete.test.ts index 61306a0b66a3b..4706301eb0fca 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_is_loading_complete.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_is_loading_complete.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useIsLoadingComplete } from './use_is_loading_complete'; describe('useIsLoadingComplete', () => { diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts index 87d0c375149e0..816b3c6af6ec2 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts @@ -22,7 +22,7 @@ export async function getEntityGroupsBy({ inventoryEsClient: ObservabilityElasticsearchClient; field: string; esQuery?: QueryDslQueryContainer; -}) { +}): Promise<EntityGroup[]> { const from = `FROM ${ENTITIES_LATEST_ALIAS}`; const where = [getBuiltinEntityDefinitionIdESQLWhereClause()]; @@ -31,8 +31,14 @@ export async function getEntityGroupsBy({ const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`; const query = [from, ...where, group, sort, limit].join(' | '); - return inventoryEsClient.esql<EntityGroup>('get_entities_groups', { - query, - filter: esQuery, - }); + const { hits } = await inventoryEsClient.esql<EntityGroup, { transform: 'plain' }>( + 'get_entities_groups', + { + query, + filter: esQuery, + }, + { transform: 'plain' } + ); + + return hits; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index e944e27379ab5..c1f7894a178b1 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -7,7 +7,6 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import type { EntityInstance } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; @@ -16,14 +15,21 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql<{ - entity: Pick<EntityInstance['entity'], 'type'>; - }>('get_entity_types', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const entityTypesEsqlResponse = await inventoryEsClient.esql< + { + 'entity.type': string; + }, + { transform: 'plain' } + >( + 'get_entity_types', + { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} `, - }); + }, + { transform: 'plain' } + ); - return entityTypesEsqlResponse.map((response) => response.entity.type); + return entityTypesEsqlResponse.hits.map((response) => response['entity.type']); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index c4bf13c5ec140..9dcf17250ad68 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -6,13 +6,13 @@ */ import type { QueryDslQueryContainer, ScalarValue } from '@elastic/elasticsearch/lib/api/types'; -import type { EntityInstance } from '@kbn/entities-schema'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; +import { unflattenObject } from '@kbn/observability-utils-common/object/unflatten_object'; import { ENTITIES_LATEST_ALIAS, InventoryEntity, @@ -62,17 +62,38 @@ export async function getLatestEntities({ const query = [from, ...where, sort, limit].join(' | '); - const latestEntitiesEsqlResponse = await inventoryEsClient.esql<EntityInstance>( + const latestEntitiesEsqlResponse = await inventoryEsClient.esql< + { + 'entity.id': string; + 'entity.type': string; + 'entity.definition_id': string; + 'entity.display_name': string; + 'entity.identity_fields': string | string[]; + 'entity.last_seen_timestamp': string; + 'entity.definition_version': string; + 'entity.schema_version': string; + } & Record<string, ScalarValue | ScalarValue[]>, + { transform: 'plain' } + >( 'get_latest_entities', { query, filter: esQuery, params, - } + }, + { transform: 'plain' } ); - return latestEntitiesEsqlResponse.map((lastestEntity) => { - const { entity, ...metadata } = lastestEntity; + return latestEntitiesEsqlResponse.hits.map((latestEntity) => { + Object.keys(latestEntity).forEach((key) => { + const keyOfObject = key as keyof typeof latestEntity; + // strip out multi-field aliases + if (keyOfObject.endsWith('.text') || keyOfObject.endsWith('.keyword')) { + delete latestEntity[keyOfObject]; + } + }); + + const { entity, ...metadata } = unflattenObject(latestEntity); return { entityId: entity.id, diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index 2f59478f17c02..c3fd3971f09d2 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -17,14 +17,18 @@ export async function getHasData({ logger: Logger; }) { try { - const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const esqlResults = await inventoryEsClient.esql<{ _count: number }, { transform: 'plain' }>( + 'get_has_data', + { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS _count = COUNT(*) | LIMIT 1`, - }); + }, + { transform: 'plain' } + ); - const totalCount = esqlResults[0]._count; + const totalCount = esqlResults.hits[0]._count; return { hasData: totalCount > 0 }; } catch (e) { diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/types.ts index 397710509dab5..646d34888ae91 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/types.ts @@ -30,10 +30,8 @@ export interface InventoryRouteHandlerResources { } export interface InventoryRouteCreateOptions { - options: { - timeout?: { - idleSocket?: number; - }; - tags: Array<'access:inventory'>; + timeout?: { + idleSocket?: number; }; + tags: Array<'access:inventory'>; } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts index afb022cdc9b7f..0cee1b701539f 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts @@ -53,10 +53,7 @@ export interface InvestigateAppRouteHandlerResources { } export interface InvestigateAppRouteCreateOptions { - options: { - timeout?: { - idleSocket?: number; - }; - tags: []; + timeout?: { + idleSocket?: number; }; } diff --git a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_error_rate_timeseries/get_logs_error_rate_timeseries.ts b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_error_rate_timeseries/get_logs_error_rate_timeseries.ts index 4766436fdebfc..ac4f62932b64a 100644 --- a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_error_rate_timeseries/get_logs_error_rate_timeseries.ts +++ b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_error_rate_timeseries/get_logs_error_rate_timeseries.ts @@ -138,7 +138,7 @@ export function createGetLogErrorRateTimeseries() { const totalErrorsCount = logErrorCount + errorLogLevelErrorsCount; return { x: timeseriesBucket.key, - y: totalErrorsCount, + y: totalErrorsCount / (intervalString / 60), }; }); diff --git a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rate_timeseries/get_logs_rate_timeseries.ts b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rate_timeseries/get_logs_rate_timeseries.ts index cdcf360405e75..9df26b48db098 100644 --- a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rate_timeseries/get_logs_rate_timeseries.ts +++ b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rate_timeseries/get_logs_rate_timeseries.ts @@ -104,12 +104,10 @@ export function createGetLogsRateTimeseries() { return buckets ? buckets.reduce<LogsRateTimeseriesReturnType>((acc, bucket) => { - const totalCount = bucket.doc_count; - const timeseries = bucket.timeseries.buckets.map((timeseriesBucket) => { return { x: timeseriesBucket.key, - y: timeseriesBucket.doc_count / totalCount, + y: timeseriesBucket.doc_count / (intervalString / 60), }; }); diff --git a/x-pack/plugins/observability_solution/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx b/x-pack/plugins/observability_solution/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx index 183950edf9777..8635df14731a5 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; // We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` import { coreMock as mockCoreMock } from '@kbn/core/public/mocks'; @@ -56,14 +56,14 @@ describe('useLogSummary hook', () => { .mockResolvedValueOnce(firstMockResponse) .mockResolvedValueOnce(secondMockResponse); - const { result, waitForNextUpdate, rerender } = renderHook( + const { result, rerender } = renderHook( ({ logViewReference }) => useLogSummary(logViewReference, startTimestamp, endTimestamp, null), { initialProps: { logViewReference: LOG_VIEW_REFERENCE }, } ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( @@ -75,7 +75,7 @@ describe('useLogSummary hook', () => { expect(result.current.buckets).toEqual(firstMockResponse.data.buckets); rerender({ logViewReference: CHANGED_LOG_VIEW_REFERENCE }); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( @@ -101,7 +101,7 @@ describe('useLogSummary hook', () => { .mockResolvedValueOnce(firstMockResponse) .mockResolvedValueOnce(secondMockResponse); - const { result, waitForNextUpdate, rerender } = renderHook( + const { result, rerender } = renderHook( ({ filterQuery }) => useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, filterQuery), { @@ -109,7 +109,7 @@ describe('useLogSummary hook', () => { } ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( @@ -121,7 +121,7 @@ describe('useLogSummary hook', () => { expect(result.current.buckets).toEqual(firstMockResponse.data.buckets); rerender({ filterQuery: 'CHANGED_FILTER_QUERY' }); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( @@ -139,7 +139,7 @@ describe('useLogSummary hook', () => { .mockResolvedValueOnce(createMockResponse([])); const firstRange = createMockDateRange(); - const { waitForNextUpdate, rerender } = renderHook( + const { rerender } = renderHook( ({ startTimestamp, endTimestamp }) => useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null), { @@ -147,7 +147,7 @@ describe('useLogSummary hook', () => { } ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -160,7 +160,7 @@ describe('useLogSummary hook', () => { const secondRange = createMockDateRange('now-20s', 'now'); rerender(secondRange); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( @@ -176,7 +176,7 @@ describe('useLogSummary hook', () => { fetchLogSummaryMock.mockResolvedValueOnce(createMockResponse([])); const firstRange = createMockDateRange(); - const { waitForNextUpdate, rerender } = renderHook( + const { rerender } = renderHook( ({ startTimestamp, endTimestamp }) => useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null), { @@ -188,7 +188,7 @@ describe('useLogSummary hook', () => { // intentionally don't wait for an update to test the throttling rerender(secondRange); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( diff --git a/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_data_search_request.test.tsx b/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_data_search_request.test.tsx index 33369c1125e15..08907b1627086 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_data_search_request.test.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_data_search_request.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import React from 'react'; import { firstValueFrom, Observable, of, Subject } from 'rxjs'; import type { ISearchGeneric, IKibanaSearchResponse } from '@kbn/search-types'; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_latest_partial_data_search_response.test.tsx b/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_latest_partial_data_search_response.test.tsx index a4b01be03d80b..24433f23bc677 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_latest_partial_data_search_response.test.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/utils/data_search/use_latest_partial_data_search_response.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { IKibanaSearchRequest } from '@kbn/search-types'; import { ParsedDataSearchRequestDescriptor, ParsedKibanaSearchResponse } from './types'; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/container/use_container_metrics_table.test.ts b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/container/use_container_metrics_table.test.ts index ec22fe1df6f91..d9da73336bf0a 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/container/use_container_metrics_table.test.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/container/use_container_metrics_table.test.ts @@ -7,7 +7,7 @@ import { useContainerMetricsTable } from './use_container_metrics_table'; import { useInfrastructureNodeMetrics } from '../shared'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMetricsClientMock } from '../test_helpers'; jest.mock('../shared', () => ({ @@ -33,6 +33,12 @@ describe('useContainerMetricsTable hook', () => { }, }; + // include this to prevent rendering error in test + useInfrastructureNodeMetricsMock.mockReturnValue({ + isLoading: true, + data: { state: 'empty-indices' }, + }); + renderHook(() => useContainerMetricsTable({ timerange: { from: 'now-30d', to: 'now' }, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/host/use_host_metrics_table.test.ts b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/host/use_host_metrics_table.test.ts index f34fccc6e442e..ab21b0c1ca76c 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/host/use_host_metrics_table.test.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/host/use_host_metrics_table.test.ts @@ -7,7 +7,7 @@ import { useHostMetricsTable } from './use_host_metrics_table'; import { useInfrastructureNodeMetrics } from '../shared'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMetricsClientMock } from '../test_helpers'; jest.mock('../shared', () => ({ @@ -40,6 +40,12 @@ describe('useHostMetricsTable hook', () => { }, }; + // include this to prevent rendering error in test + useInfrastructureNodeMetricsMock.mockReturnValue({ + isLoading: true, + data: { state: 'empty-indices' }, + }); + renderHook(() => useHostMetricsTable({ timerange: { from: 'now-30d', to: 'now' }, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/pod/use_pod_metrics_table.test.ts b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/pod/use_pod_metrics_table.test.ts index fc6d14b03d08b..14f3330e856ad 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/pod/use_pod_metrics_table.test.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/components/infrastructure_node_metrics_tables/pod/use_pod_metrics_table.test.ts @@ -7,7 +7,7 @@ import { usePodMetricsTable } from './use_pod_metrics_table'; import { useInfrastructureNodeMetrics } from '../shared'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createMetricsClientMock } from '../test_helpers'; jest.mock('../shared', () => ({ @@ -33,6 +33,12 @@ describe('usePodMetricsTable hook', () => { }, }; + // include this to prevent rendering error in test + useInfrastructureNodeMetricsMock.mockReturnValue({ + isLoading: true, + data: { state: 'empty-indices' }, + }); + renderHook(() => usePodMetricsTable({ timerange: { from: 'now-30d', to: 'now' }, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/pages/link_to/use_asset_detail_redirect.test.tsx b/x-pack/plugins/observability_solution/metrics_data_access/public/pages/link_to/use_asset_detail_redirect.test.tsx index cf783e78bd67e..1183979d60581 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/pages/link_to/use_asset_detail_redirect.test.tsx +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/pages/link_to/use_asset_detail_redirect.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { coreMock } from '@kbn/core/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; diff --git a/x-pack/plugins/observability_solution/observability/kibana.jsonc b/x-pack/plugins/observability_solution/observability/kibana.jsonc index 1c09efd7dd6e1..3a888ce14e5ef 100644 --- a/x-pack/plugins/observability_solution/observability/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability/kibana.jsonc @@ -56,7 +56,8 @@ "serverless", "guidedOnboarding", "observabilityAIAssistant", - "investigate" + "investigate", + "streams" ], "requiredBundles": [ "data", @@ -70,4 +71,4 @@ "common" ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts index a4825c11f85cd..0aedbb5723219 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.test.ts @@ -75,6 +75,32 @@ const useCases = [ sourceField: '', }, ], + [ + { + aggType: Aggregators.COUNT, + field: '', + filter: `container.name:container's name-1`, + name: '', + }, + { + operation: 'count', + operationWithField: `count(kql='container.name:container\\'s name-1')`, + sourceField: '', + }, + ], + [ + { + aggType: Aggregators.COUNT, + field: '', + filter: 'host.name: host-*', + name: '', + }, + { + operation: 'count', + operationWithField: `count(kql='host.name: host-*')`, + sourceField: '', + }, + ], [ { aggType: Aggregators.CARDINALITY, @@ -136,7 +162,7 @@ const useCases = [ }, { operation: 'counter_rate', - operationWithField: `counter_rate(max(system.network.in.bytes), kql='host.name : foo')`, + operationWithField: `counter_rate(max(system.network.in.bytes), kql='host.name : "foo"')`, sourceField: 'system.network.in.bytes', }, ], diff --git a/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts index 9eb52af738ea9..30663d02cda72 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/rule_condition_chart/helpers.ts @@ -15,15 +15,15 @@ export interface LensOperation { } export const getLensOperationFromRuleMetric = (metric: GenericMetric): LensOperation => { - const { aggType, field, filter } = metric; + const { aggType, field, filter = '' } = metric; let operation: string = aggType; const operationArgs: string[] = []; - const aggFilter = JSON.stringify(filter || '').replace(/"|\\/g, ''); + const escapedFilter = filter.replace(/'/g, "\\'"); if (aggType === Aggregators.RATE) { return { operation: 'counter_rate', - operationWithField: `counter_rate(max(${field}), kql='${aggFilter}')`, + operationWithField: `counter_rate(max(${field}), kql='${escapedFilter}')`, sourceField: field || '', }; } @@ -45,8 +45,7 @@ export const getLensOperationFromRuleMetric = (metric: GenericMetric): LensOpera operationArgs.push('percentile=99'); } - if (aggFilter) operationArgs.push(`kql='${aggFilter}'`); - + if (escapedFilter) operationArgs.push(`kql='${escapedFilter}'`); return { operation, operationWithField: `${operation}(${operationArgs.join(', ')})`, diff --git a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts index 07bb33ebb5a98..16718648c9724 100644 --- a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts +++ b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; import type { AddSolutionNavigationArg } from '@kbn/navigation-plugin/public'; -import { of } from 'rxjs'; +import { map, of } from 'rxjs'; import type { ObservabilityPublicPluginsStart } from './plugin'; const title = i18n.translate( @@ -18,7 +18,7 @@ const title = i18n.translate( ); const icon = 'logoObservability'; -export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { +function createNavTree({ streamsAvailable }: { streamsAvailable?: boolean }) { const navTree: NavigationTreeDefinition = { body: [ { @@ -87,6 +87,13 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { link: 'inventory', spaceBefore: 'm', }, + ...(streamsAvailable + ? [ + { + link: 'streams' as const, + }, + ] + : []), { id: 'apm', title: i18n.translate('xpack.observability.obltNav.applications', { @@ -558,6 +565,8 @@ export const createDefinition = ( title, icon: 'logoObservability', homePage: 'observabilityOnboarding', - navigationTree$: of(createNavTree(pluginsStart)), + navigationTree$: (pluginsStart.streams?.status$ || of({ status: 'disabled' as const })).pipe( + map(({ status }) => createNavTree({ streamsAvailable: status === 'enabled' })) + ), dataTestSubj: 'observabilitySideNav', }); diff --git a/x-pack/plugins/observability_solution/observability/public/plugin.ts b/x-pack/plugins/observability_solution/observability/public/plugin.ts index 5866a082556bb..c37f3cc2f624a 100644 --- a/x-pack/plugins/observability_solution/observability/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/public/plugin.ts @@ -70,6 +70,7 @@ import type { import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import type { StreamsPluginStart, StreamsPluginSetup } from '@kbn/streams-plugin/public'; import { observabilityAppId, observabilityFeatureId } from '../common'; import { ALERTS_PATH, @@ -124,6 +125,7 @@ export interface ObservabilityPublicPluginsSetup { licensing: LicensingPluginSetup; serverless?: ServerlessPluginSetup; presentationUtil?: PresentationUtilPluginStart; + streams?: StreamsPluginSetup; } export interface ObservabilityPublicPluginsStart { actionTypeRegistry: ActionTypeRegistryContract; @@ -162,6 +164,7 @@ export interface ObservabilityPublicPluginsStart { dataViewFieldEditor: DataViewFieldEditorStart; toastNotifications: ToastsStart; investigate?: InvestigatePublicStart; + streams?: StreamsPluginStart; } export type ObservabilityPublicStart = ReturnType<Plugin['start']>; diff --git a/x-pack/plugins/observability_solution/observability/server/plugin.ts b/x-pack/plugins/observability_solution/observability/server/plugin.ts index b98fe316c712e..4d0e809f882e7 100644 --- a/x-pack/plugins/observability_solution/observability/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/server/plugin.ts @@ -195,7 +195,6 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> { void core.getStartServices().then(([coreStart, pluginStart]) => { registerRoutes({ core, - config, dependencies: { pluginsSetup: { ...plugins, diff --git a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts index 5599039a5ce67..9ce2d7c9f1829 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts @@ -4,29 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { errors } from '@elastic/elasticsearch'; -import Boom from '@hapi/boom'; import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; -import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server'; +import { CoreSetup, KibanaRequest, Logger } from '@kbn/core/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; -import { - IoTsParamsObject, - decodeRequestParams, - parseEndpoint, - passThroughValidationObject, - stripNullishRequestParameters, -} from '@kbn/server-route-repository'; +import { registerRoutes as registerServerRoutes } from '@kbn/server-route-repository'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; -import axios from 'axios'; -import * as t from 'io-ts'; -import { ObservabilityConfig } from '..'; import { AlertDetailsContextualInsightsService } from '../services'; -import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; interface RegisterRoutes { - config: ObservabilityConfig; core: CoreSetup; repository: AbstractObservabilityServerRouteRepository; logger: Logger; @@ -46,81 +33,11 @@ export interface RegisterRoutesDependencies { getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi; } -export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) { - const routes = Object.values(repository); - - const router = core.http.createRouter(); - - routes.forEach((route) => { - const { endpoint, options, handler, params } = route; - const { pathname, method } = parseEndpoint(endpoint); - - (router[method] as RouteRegistrar<typeof method, ObservabilityRequestHandlerContext>)( - { - path: pathname, - validate: passThroughValidationObject, - options, - }, - async (context, request, response) => { - try { - const decodedParams = decodeRequestParams( - stripNullishRequestParameters({ - params: request.params, - body: request.body, - query: request.query, - }), - (params as IoTsParamsObject) ?? t.strict({}) - ); - - const data = await handler({ - config, - context, - request, - logger, - params: decodedParams, - dependencies, - }); - - if (data === undefined) { - return response.noContent(); - } - - return response.ok({ body: data }); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error(error); - return response.customError({ - statusCode: error.response?.status || 500, - body: { - message: error.message, - }, - }); - } - - if (Boom.isBoom(error)) { - logger.error(error.output.payload.message); - return response.customError({ - statusCode: error.output.statusCode, - body: { message: error.output.payload.message }, - }); - } - - logger.error(error); - const opts = { - statusCode: 500, - body: { - message: error.message, - }, - }; - - if (error instanceof errors.RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; - } - - return response.customError(opts); - } - } - ); +export function registerRoutes({ repository, core, logger, dependencies }: RegisterRoutes) { + registerServerRoutes({ + core, + dependencies: { dependencies }, + logger, + repository, }); } diff --git a/x-pack/plugins/observability_solution/observability/server/routes/types.ts b/x-pack/plugins/observability_solution/observability/server/routes/types.ts index 3940253137640..111bc4e714119 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/types.ts @@ -13,7 +13,6 @@ import { } from './get_global_observability_server_route_repository'; import { ObservabilityRequestHandlerContext } from '../types'; import { RegisterRoutesDependencies } from './register_routes'; -import { ObservabilityConfig } from '..'; export type { ObservabilityServerRouteRepository, APIEndpoint }; @@ -22,14 +21,11 @@ export interface ObservabilityRouteHandlerResources { dependencies: RegisterRoutesDependencies; logger: Logger; request: KibanaRequest; - config: ObservabilityConfig; } export interface ObservabilityRouteCreateOptions { - options: { - tags: string[]; - access?: 'public' | 'internal'; - }; + tags: string[]; + access?: 'public' | 'internal'; } export type AbstractObservabilityServerRouteRepository = ServerRouteRepository; diff --git a/x-pack/plugins/observability_solution/observability/tsconfig.json b/x-pack/plugins/observability_solution/observability/tsconfig.json index 84a691f1033af..b3c18dc24b8e0 100644 --- a/x-pack/plugins/observability_solution/observability/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability/tsconfig.json @@ -111,6 +111,7 @@ "@kbn/core-ui-settings-server-mocks", "@kbn/es-types", "@kbn/logging-mocks", + "@kbn/streams-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts index 80ddf3cbc0a0d..1bf892c0d40ee 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts @@ -34,7 +34,7 @@ export function registerContextFunction({ visibility: FunctionVisibility.Internal, }, async ({ messages, screenContexts, chat }, signal) => { - const { analytics } = (await resources.context.core).coreStart; + const { analytics } = await resources.plugins.core.start(); async function getContext() { const screenDescription = compact( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/get_dataset_info/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/get_dataset_info/index.ts index 57cac3a4e0c0f..77ba9afb18260 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/get_dataset_info/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/get_dataset_info/index.ts @@ -48,7 +48,7 @@ export function registerGetDatasetInfoFunction({ try { const body = await esClient.asCurrentUser.indices.resolveIndex({ - name: index === '' ? '*' : index.split(','), + name: index === '' ? ['*', '*:*'] : index.split(','), expand_wildcards: 'open', }); indices = [ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index f693fa53c06cc..7949276ac6aba 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -111,7 +111,18 @@ export class ObservabilityAIAssistantPlugin ]; }), }; - }) as ObservabilityAIAssistantRouteHandlerResources['plugins']; + }) as Pick< + ObservabilityAIAssistantRouteHandlerResources['plugins'], + keyof ObservabilityAIAssistantPluginStartDependencies + >; + + const withCore = { + ...routeHandlerPlugins, + core: { + setup: core, + start: () => core.getStartServices().then(([coreStart]) => coreStart), + }, + }; const service = (this.service = new ObservabilityAIAssistantService({ logger: this.logger.get('service'), @@ -133,7 +144,7 @@ export class ObservabilityAIAssistantPlugin core, logger: this.logger, dependencies: { - plugins: routeHandlerPlugins, + plugins: withCore, service: this.service, }, }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts index a6fe57cb58adc..e80e6fa156b06 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts @@ -194,7 +194,7 @@ const chatRecallRoute = createObservabilityAIAssistantServerRoute({ const response$ = from( recallAndScore({ - analytics: (await resources.context.core).coreStart.analytics, + analytics: (await resources.plugins.core.start()).analytics, chat: (name, params) => client .chat(name, { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts index 1a6140968c925..27c7361e8a7fb 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts @@ -6,7 +6,7 @@ */ import type { CoreSetup } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { registerRoutes } from '@kbn/server-route-repository'; +import { DefaultRouteHandlerResources, registerRoutes } from '@kbn/server-route-repository'; import { getGlobalObservabilityAIAssistantServerRouteRepository } from './get_global_observability_ai_assistant_route_repository'; import type { ObservabilityAIAssistantRouteHandlerResources } from './types'; import { ObservabilityAIAssistantPluginStartDependencies } from '../types'; @@ -20,7 +20,7 @@ export function registerServerRoutes({ logger: Logger; dependencies: Omit< ObservabilityAIAssistantRouteHandlerResources, - 'request' | 'context' | 'logger' | 'params' + keyof DefaultRouteHandlerResources >; }) { registerRoutes({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts index b817328d22c64..62365536f3823 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts @@ -6,32 +6,36 @@ */ import type { + CoreSetup, CoreStart, CustomRequestHandlerContext, IScopedClusterClient, IUiSettingsClient, - KibanaRequest, SavedObjectsClientContract, } from '@kbn/core/server'; -import type { Logger } from '@kbn/logging'; import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server/types'; import type { RacApiRequestHandlerContext } from '@kbn/rule-registry-plugin/server'; import type { RulesClientApi } from '@kbn/alerting-plugin/server/types'; +import { DefaultRouteHandlerResources } from '@kbn/server-route-repository-utils'; import type { ObservabilityAIAssistantService } from '../service'; import type { ObservabilityAIAssistantPluginSetupDependencies, ObservabilityAIAssistantPluginStartDependencies, } from '../types'; +type ObservabilityAIAssistantRequestHandlerContextBase = CustomRequestHandlerContext<{ + licensing: Pick<LicensingApiRequestHandlerContext, 'license' | 'featureUsage'>; + // these two are here for compatibility with APM functions + rac: Pick<RacApiRequestHandlerContext, 'getAlertsClient'>; + alerting: { + getRulesClient: () => RulesClientApi; + }; +}>; + +// this is the type used across methods, it's stripped down for compatibility +// with the context that's available when executing as an action export type ObservabilityAIAssistantRequestHandlerContext = Omit< - CustomRequestHandlerContext<{ - licensing: Pick<LicensingApiRequestHandlerContext, 'license' | 'featureUsage'>; - // these two are here for compatibility with APM functions - rac: Pick<RacApiRequestHandlerContext, 'getAlertsClient'>; - alerting: { - getRulesClient: () => RulesClientApi; - }; - }>, + ObservabilityAIAssistantRequestHandlerContextBase, 'core' | 'resolve' > & { core: Promise<{ @@ -45,32 +49,41 @@ export type ObservabilityAIAssistantRequestHandlerContext = Omit< savedObjects: { client: SavedObjectsClientContract; }; - coreStart: CoreStart; }>; }; -export interface ObservabilityAIAssistantRouteHandlerResources { - request: KibanaRequest; +interface PluginContractResolveCore { + core: { + setup: CoreSetup<ObservabilityAIAssistantPluginStartDependencies>; + start: () => Promise<CoreStart>; + }; +} + +type PluginContractResolveDependenciesStart = { + [key in keyof ObservabilityAIAssistantPluginStartDependencies]: { + start: () => Promise<Required<ObservabilityAIAssistantPluginStartDependencies>[key]>; + }; +}; + +type PluginContractResolveDependenciesSetup = { + [key in keyof ObservabilityAIAssistantPluginSetupDependencies]: { + setup: Required<ObservabilityAIAssistantPluginSetupDependencies>[key]; + }; +}; + +export interface ObservabilityAIAssistantRouteHandlerResources + extends Omit<DefaultRouteHandlerResources, 'context' | 'response'> { context: ObservabilityAIAssistantRequestHandlerContext; - logger: Logger; service: ObservabilityAIAssistantService; - plugins: { - [key in keyof ObservabilityAIAssistantPluginSetupDependencies]: { - setup: Required<ObservabilityAIAssistantPluginSetupDependencies>[key]; - }; - } & { - [key in keyof ObservabilityAIAssistantPluginStartDependencies]: { - start: () => Promise<Required<ObservabilityAIAssistantPluginStartDependencies>[key]>; - }; - }; + plugins: PluginContractResolveCore & + PluginContractResolveDependenciesSetup & + PluginContractResolveDependenciesStart; } export interface ObservabilityAIAssistantRouteCreateOptions { - options: { - timeout?: { - payload?: number; - idleSocket?: number; - }; - tags: Array<'access:ai_assistant'>; + timeout?: { + payload?: number; + idleSocket?: number; }; + tags: Array<'access:ai_assistant'>; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json index d5acd7a365b50..709b3117d575d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json @@ -47,6 +47,7 @@ "@kbn/ai-assistant-common", "@kbn/inference-common", "@kbn/core-lifecycle-server", + "@kbn/server-route-repository-utils", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/functions/visualize_esql.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/functions/visualize_esql.tsx index a570d4ba0276a..9a4cf790b85cd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/functions/visualize_esql.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/functions/visualize_esql.tsx @@ -127,13 +127,13 @@ export function VisualizeESQL({ ( isLoading: boolean, adapters: InlineEditLensEmbeddableContext['lensEvent']['adapters'] | undefined, - lensEmbeddableOutput$?: InlineEditLensEmbeddableContext['lensEvent']['embeddableOutput$'] + dataLoading$?: InlineEditLensEmbeddableContext['lensEvent']['dataLoading$'] ) => { const adapterTables = adapters?.tables?.tables; if (adapterTables && !isLoading) { setLensLoadEvent({ adapters, - embeddableOutput$: lensEmbeddableOutput$, + dataLoading$, }); } }, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts index 63e06818a2b70..97fdc01069b97 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts @@ -17,7 +17,6 @@ import { ObservabilityAIAssistantRequestHandlerContext, ObservabilityAIAssistantRouteHandlerResources, } from '@kbn/observability-ai-assistant-plugin/server/routes/types'; -import { ObservabilityAIAssistantPluginStartDependencies } from '@kbn/observability-ai-assistant-plugin/server/types'; import { mapValues } from 'lodash'; import { firstValueFrom } from 'rxjs'; import type { ObservabilityAIAssistantAppConfig } from './config'; @@ -59,13 +58,22 @@ export class ObservabilityAIAssistantAppPlugin setup: value, start: () => core.getStartServices().then((services) => { - const [, pluginsStartContracts] = services; + const [_, pluginsStartContracts] = services; + return pluginsStartContracts[ - key as keyof ObservabilityAIAssistantPluginStartDependencies + key as keyof ObservabilityAIAssistantAppPluginStartDependencies ]; }), }; - }) as ObservabilityAIAssistantRouteHandlerResources['plugins']; + }) as Omit<ObservabilityAIAssistantRouteHandlerResources['plugins'], 'core'>; + + const withCore = { + ...routeHandlerPlugins, + core: { + setup: core, + start: () => core.getStartServices().then(([coreStart]) => coreStart), + }, + }; const initResources = async ( request: KibanaRequest @@ -90,7 +98,6 @@ export class ObservabilityAIAssistantAppPlugin }; }), core: Promise.resolve({ - coreStart, elasticsearch: { client: coreStart.elasticsearch.client.asScoped(request), }, @@ -110,7 +117,7 @@ export class ObservabilityAIAssistantAppPlugin context, service: plugins.observabilityAIAssistant.service, logger: this.logger.get('connector'), - plugins: routeHandlerPlugins, + plugins: withCore, }; }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts index de02e4cf841ce..04fd10c3e506f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts @@ -94,12 +94,14 @@ describe('observabilityAIAssistant rule_connector', () => { getAdhocInstructions: () => [], }), }, - context: { - core: Promise.resolve({ - coreStart: { http: { basePath: { publicBaseUrl: 'http://kibana.com' } } }, - }), - }, + context: {}, plugins: { + core: { + start: () => + Promise.resolve({ + http: { basePath: { publicBaseUrl: 'http://kibana.com' } }, + }), + }, actions: { start: async () => { return { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts index 33f3bdd2c98f8..1f5a097f8f7cd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts @@ -248,7 +248,7 @@ If available, include the link of the conversation at the end of your answer.` isPublic: true, connectorId: execOptions.params.connector, signal: new AbortController().signal, - kibanaPublicUrl: (await resources.context.core).coreStart.http.basePath.publicBaseUrl, + kibanaPublicUrl: (await resources.plugins.core.start()).http.basePath.publicBaseUrl, instructions: [backgroundInstruction], messages: [ { diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/footer/footer.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/footer/footer.tsx index dae5f70bf3db0..8f8b0ec853dc7 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/footer/footer.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/footer/footer.tsx @@ -43,6 +43,7 @@ export const Footer: FunctionComponent = () => { { defaultMessage: 'Explore demo' } ), link: URL_DEMO_ENV, + testSubject: 'observabilityOnboardingFooterExploreDemoLink', }, { iconUrl: forumIconUrl, @@ -65,6 +66,7 @@ export const Footer: FunctionComponent = () => { { defaultMessage: 'Open Elastic Discuss forum' } ), link: URL_FORUM, + testSubject: 'observabilityOnboardingFooterDiscussForumLink', }, { iconUrl: docsIconUrl, @@ -87,6 +89,7 @@ export const Footer: FunctionComponent = () => { { defaultMessage: 'Learn more about all Elastic features' } ), link: docLinks.links.observability.guide, + testSubject: 'observabilityOnboardingFooterLearnMoreLink', }, { iconUrl: supportIconUrl, @@ -105,6 +108,7 @@ export const Footer: FunctionComponent = () => { { defaultMessage: 'Open Support Hub' } ), link: helpSupportUrl, + testSubject: 'observabilityOnboardingFooterOpenSupportHubLink', }, ]; @@ -127,7 +131,7 @@ export const Footer: FunctionComponent = () => { <EuiText size="xs"> <p> <EuiLink - data-test-subj="observabilityOnboardingFooterLearnMoreLink" + data-test-subj={section.testSubject} aria-label={section.linkARIALabel} href={section.link} target="_blank" diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/plugin.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/plugin.ts index ccb260a002cf2..30aaaf2588388 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/plugin.ts @@ -14,8 +14,8 @@ import type { } from '@kbn/core/server'; import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { DefaultRouteHandlerResources, registerRoutes } from '@kbn/server-route-repository'; import { getObservabilityOnboardingServerRouteRepository } from './routes'; -import { registerRoutes } from './routes/register_routes'; import { ObservabilityOnboardingRouteHandlerResources } from './routes/types'; import { ObservabilityOnboardingPluginSetup, @@ -71,16 +71,28 @@ export class ObservabilityOnboardingPlugin }) as ObservabilityOnboardingRouteHandlerResources['plugins']; const config = this.initContext.config.get<ObservabilityOnboardingConfig>(); - registerRoutes({ - core, - logger: this.logger, - repository: getObservabilityOnboardingServerRouteRepository(), - plugins: resourcePlugins, + + const dependencies: Omit< + ObservabilityOnboardingRouteHandlerResources, + keyof DefaultRouteHandlerResources + > = { config, kibanaVersion: this.initContext.env.packageInfo.version, + plugins: resourcePlugins, services: { esLegacyConfigService: this.esLegacyConfigService, }, + core: { + setup: core, + start: () => core.getStartServices().then(([coreStart]) => coreStart), + }, + }; + + registerRoutes({ + core, + logger: this.logger, + repository: getObservabilityOnboardingServerRouteRepository(), + dependencies, }); plugins.customIntegrations.registerCustomIntegration({ diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts index 372eaf7972373..738c9f1fefd57 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts @@ -13,7 +13,8 @@ import { type PackageClient, } from '@kbn/fleet-plugin/server'; import { dump } from 'js-yaml'; -import { PackageDataStreamTypes } from '@kbn/fleet-plugin/common/types'; +import { PackageDataStreamTypes, Output } from '@kbn/fleet-plugin/common/types'; +import { transformOutputToFullPolicyOutput } from '@kbn/fleet-plugin/server/services/output_client'; import { getObservabilityOnboardingFlow, saveObservabilityOnboardingFlow } from '../../lib/state'; import type { SavedObservabilityOnboardingFlow } from '../../saved_objects/observability_onboarding_status'; import { ObservabilityOnboardingFlow } from '../../saved_objects/observability_onboarding_status'; @@ -21,7 +22,6 @@ import { createObservabilityOnboardingServerRoute } from '../create_observabilit import { getHasLogs } from './get_has_logs'; import { getKibanaUrl } from '../../lib/get_fallback_urls'; import { getAgentVersionInfo } from '../../lib/get_agent_version'; -import { getFallbackESUrl } from '../../lib/get_fallback_urls'; import { ElasticAgentStepPayload, InstalledIntegration, StepProgressPayloadRT } from '../types'; import { createShipperApiKey } from '../../lib/api_key/create_shipper_api_key'; import { createInstallApiKey } from '../../lib/api_key/create_install_api_key'; @@ -329,6 +329,13 @@ const integrationsInstallRoute = createObservabilityOnboardingServerRoute({ throw Boom.notFound(`Onboarding session '${params.path.onboardingId}' not found.`); } + const outputClient = await fleetStart.createOutputClient(request); + const defaultOutputId = await outputClient.getDefaultDataOutputId(); + if (!defaultOutputId) { + throw Boom.notFound('Default data output not found'); + } + const output = await outputClient.get(defaultOutputId); + const integrationsToInstall = parseIntegrationsTSV(params.body); if (!integrationsToInstall.length) { return response.badRequest({ @@ -381,15 +388,11 @@ const integrationsInstallRoute = createObservabilityOnboardingServerRoute({ }, }); - const elasticsearchUrl = plugins.cloud?.setup?.elasticsearchUrl - ? [plugins.cloud?.setup?.elasticsearchUrl] - : await getFallbackESUrl(services.esLegacyConfigService); - return response.ok({ headers: { 'content-type': 'application/x-tar', }, - body: generateAgentConfigTar({ elasticsearchUrl, installedIntegrations }), + body: generateAgentConfigTar(output, installedIntegrations), }); }, }); @@ -565,14 +568,9 @@ function parseRegistryIntegrationMetadata( } } -const generateAgentConfigTar = ({ - elasticsearchUrl, - installedIntegrations, -}: { - elasticsearchUrl: string[]; - installedIntegrations: InstalledIntegration[]; -}) => { +function generateAgentConfigTar(output: Output, installedIntegrations: InstalledIntegration[]) { const now = new Date(); + return makeTar([ { type: 'File', @@ -581,11 +579,7 @@ const generateAgentConfigTar = ({ mtime: now, data: dump({ outputs: { - default: { - type: 'elasticsearch', - hosts: elasticsearchUrl, - api_key: '${API_KEY}', // Placeholder to be replaced by bash script with the actual API key - }, + default: transformOutputToFullPolicyOutput(output, undefined, true), }, }), }, @@ -603,7 +597,7 @@ const generateAgentConfigTar = ({ data: integration.config, })), ]); -}; +} export const flowRouteRepository = { ...createFlowRoute, diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts deleted file mode 100644 index 8fe51623510eb..0000000000000 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts +++ /dev/null @@ -1,126 +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 { errors } from '@elastic/elasticsearch'; -import Boom from '@hapi/boom'; -import type { IKibanaResponse } from '@kbn/core/server'; -import { CoreSetup, Logger, RouteRegistrar } from '@kbn/core/server'; -import { - IoTsParamsObject, - ServerRouteRepository, - decodeRequestParams, - stripNullishRequestParameters, - parseEndpoint, - passThroughValidationObject, -} from '@kbn/server-route-repository'; -import * as t from 'io-ts'; -import { ObservabilityOnboardingConfig } from '..'; -import { EsLegacyConfigService } from '../services/es_legacy_config_service'; -import { ObservabilityOnboardingRequestHandlerContext } from '../types'; -import { ObservabilityOnboardingRouteHandlerResources } from './types'; - -interface RegisterRoutes { - core: CoreSetup; - repository: ServerRouteRepository; - logger: Logger; - plugins: ObservabilityOnboardingRouteHandlerResources['plugins']; - config: ObservabilityOnboardingConfig; - kibanaVersion: string; - services: { - esLegacyConfigService: EsLegacyConfigService; - }; -} - -export function registerRoutes({ - repository, - core, - logger, - plugins, - config, - kibanaVersion, - services, -}: RegisterRoutes) { - const routes = Object.values(repository); - - const router = core.http.createRouter(); - - routes.forEach((route) => { - const { endpoint, options, handler, params } = route; - const { pathname, method } = parseEndpoint(endpoint); - - (router[method] as RouteRegistrar<typeof method, ObservabilityOnboardingRequestHandlerContext>)( - { - path: pathname, - validate: passThroughValidationObject, - options, - }, - async (context, request, response) => { - try { - const decodedParams = decodeRequestParams( - stripNullishRequestParameters({ - params: request.params, - body: request.body, - query: request.query, - }), - (params as IoTsParamsObject) ?? t.strict({}) - ); - - const data = (await handler({ - context, - request, - response, - logger, - params: decodedParams, - plugins, - core: { - setup: core, - start: async () => { - const [coreStart] = await core.getStartServices(); - return coreStart; - }, - }, - config, - kibanaVersion, - services, - })) as any; - - if (data === undefined) { - return response.noContent(); - } - - if (data instanceof response.noContent().constructor) { - return data as IKibanaResponse; - } - - return response.ok({ body: data }); - } catch (error) { - if (Boom.isBoom(error)) { - logger.error(error.output.payload.message); - return response.customError({ - statusCode: error.output.statusCode, - body: { message: error.output.payload.message }, - }); - } - - logger.error(error); - const opts = { - statusCode: 500, - body: { - message: error.message, - }, - }; - - if (error instanceof errors.RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; - } - - return response.customError(opts); - } - } - ); - }); -} diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/types.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/types.ts index 4b35272eaa330..689ab14739818 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/types.ts @@ -46,10 +46,8 @@ export interface ObservabilityOnboardingRouteHandlerResources { } export interface ObservabilityOnboardingRouteCreateOptions { - options: { - tags: string[]; - xsrfRequired?: boolean; - }; + tags: string[]; + xsrfRequired?: boolean; } export const IntegrationRT = t.intersection([ diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts index 4d8be9efc59c6..db3e91fbf493a 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts @@ -6,11 +6,11 @@ */ const createKubernetesEntity = <T extends string>(base: T) => ({ - ecs: `kubernetes_${base}_ecs` as const, - semconv: `kubernetes_${base}_semconv` as const, + ecs: `k8s.${base}.ecs` as const, + semconv: `k8s.${base}.semconv` as const, }); -export const ENTITY_TYPES = { +export const BUILT_IN_ENTITY_TYPES = { HOST: 'host', CONTAINER: 'container', SERVICE: 'service', @@ -18,12 +18,13 @@ export const ENTITY_TYPES = { CLUSTER: createKubernetesEntity('cluster'), CONTAINER: createKubernetesEntity('container'), CRONJOB: createKubernetesEntity('cron_job'), - DAEMONSET: createKubernetesEntity('daemon_set'), + DAEMONSET: createKubernetesEntity('daemonset'), DEPLOYMENT: createKubernetesEntity('deployment'), JOB: createKubernetesEntity('job'), NAMESPACE: createKubernetesEntity('namespace'), NODE: createKubernetesEntity('node'), POD: createKubernetesEntity('pod'), - STATEFULSET: createKubernetesEntity('stateful_set'), + SERVICE: createKubernetesEntity('service'), + STATEFULSET: createKubernetesEntity('statefulset'), }, } as const; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts index adc07a2931b60..92ae5701f8000 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ENTITY_TYPES } from './entity_types'; +export { BUILT_IN_ENTITY_TYPES } from './entity_types'; export { EntityDataStreamType } from './entity_data_stream_types'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index f483bcc5dc269..a8e26366ab4b3 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -219,4 +219,4 @@ export { export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping'; -export { ENTITY_TYPES, EntityDataStreamType } from './entity'; +export { BUILT_IN_ENTITY_TYPES, EntityDataStreamType } from './entity'; diff --git a/x-pack/plugins/observability_solution/observability_shared/kibana.jsonc b/x-pack/plugins/observability_solution/observability_shared/kibana.jsonc index a5cde081c7c54..8e5a4c25af48c 100644 --- a/x-pack/plugins/observability_solution/observability_shared/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_shared/kibana.jsonc @@ -33,4 +33,4 @@ "common" ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/observability_solution/profiling/server/lib/setup/get_has_setup_privileges.ts b/x-pack/plugins/observability_solution/profiling/server/lib/setup/get_has_setup_privileges.ts index 83bd21b1740b8..8696c97dabd31 100644 --- a/x-pack/plugins/observability_solution/profiling/server/lib/setup/get_has_setup_privileges.ts +++ b/x-pack/plugins/observability_solution/profiling/server/lib/setup/get_has_setup_privileges.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from '@kbn/core/server'; import { INTEGRATIONS_PLUGIN_ID, PLUGIN_ID as FLEET_PLUGIN_ID } from '@kbn/fleet-plugin/common'; +import { ApiOperation } from '@kbn/security-plugin-types-server'; import { ProfilingPluginStartDeps } from '../../types'; export async function getHasSetupPrivileges({ @@ -31,8 +32,11 @@ export async function getHasSetupPrivileges({ }, }, kibana: [ - securityPluginStart.authz.actions.api.get(`${FLEET_PLUGIN_ID}-all`), - securityPluginStart.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`), + securityPluginStart.authz.actions.api.get(ApiOperation.Manage, `${FLEET_PLUGIN_ID}-all`), + securityPluginStart.authz.actions.api.get( + ApiOperation.Manage, + `${INTEGRATIONS_PLUGIN_ID}-all` + ), ], }); return hasAllRequested; diff --git a/x-pack/plugins/observability_solution/profiling/tsconfig.json b/x-pack/plugins/observability_solution/profiling/tsconfig.json index 937eee96641c8..b89d34bb8442b 100644 --- a/x-pack/plugins/observability_solution/profiling/tsconfig.json +++ b/x-pack/plugins/observability_solution/profiling/tsconfig.json @@ -54,7 +54,8 @@ "@kbn/management-settings-components-field-row", "@kbn/deeplinks-observability", "@kbn/react-kibana-context-render", - "@kbn/apm-data-access-plugin" + "@kbn/apm-data-access-plugin", + "@kbn/security-plugin-types-server" // add references to other TypeScript projects the plugin depends on // requiredPlugins from ./kibana.json diff --git a/x-pack/plugins/observability_solution/slo/public/application.tsx b/x-pack/plugins/observability_solution/slo/public/application.tsx index abd85fc712c0e..12019ae1fdf75 100644 --- a/x-pack/plugins/observability_solution/slo/public/application.tsx +++ b/x-pack/plugins/observability_solution/slo/public/application.tsx @@ -13,7 +13,6 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -53,7 +52,7 @@ export const renderApp = ({ experimentalFeatures, sloClient, }: Props) => { - const { element, history, theme$ } = appMountParameters; + const { element, history } = appMountParameters; // ensure all divs are .kbnAppWrappers element.classList.add(APP_WRAPPER_CLASS); @@ -90,42 +89,40 @@ export const renderApp = ({ ReactDOM.render( <KibanaRenderContextProvider {...core}> <ApplicationUsageTrackingProvider> - <KibanaThemeProvider {...{ theme: { theme$ } }}> - <CloudProvider> - <KibanaContextProvider - services={{ - ...core, - ...plugins, - storage: new Storage(localStorage), + <CloudProvider> + <KibanaContextProvider + services={{ + ...core, + ...plugins, + storage: new Storage(localStorage), + isDev, + kibanaVersion, + isServerless, + }} + > + <PluginContext.Provider + value={{ isDev, - kibanaVersion, isServerless, + appMountParameters, + ObservabilityPageTemplate, + observabilityRuleTypeRegistry, + experimentalFeatures, + sloClient, }} > - <PluginContext.Provider - value={{ - isDev, - isServerless, - appMountParameters, - ObservabilityPageTemplate, - observabilityRuleTypeRegistry, - experimentalFeatures, - sloClient, - }} - > - <Router history={history}> - <RedirectAppLinks coreStart={core} data-test-subj="observabilityMainContainer"> - <PerformanceContextProvider> - <QueryClientProvider client={queryClient}> - <App /> - </QueryClientProvider> - </PerformanceContextProvider> - </RedirectAppLinks> - </Router> - </PluginContext.Provider> - </KibanaContextProvider> - </CloudProvider> - </KibanaThemeProvider> + <Router history={history}> + <RedirectAppLinks coreStart={core} data-test-subj="observabilityMainContainer"> + <PerformanceContextProvider> + <QueryClientProvider client={queryClient}> + <App /> + </QueryClientProvider> + </PerformanceContextProvider> + </RedirectAppLinks> + </Router> + </PluginContext.Provider> + </KibanaContextProvider> + </CloudProvider> </ApplicationUsageTrackingProvider> </KibanaRenderContextProvider>, element diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/overview_item.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/overview_item.tsx index d26eea29f996c..f7953854b217d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/overview_item.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/overview_item.tsx @@ -9,7 +9,7 @@ import { EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { useUrlSearchState } from '../../hooks/use_url_search_state'; -export function OverViewItem({ +export function OverviewItem({ title, description, titleColor, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx index 9fba8b59bef4a..1edfba0fffb4e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx @@ -12,7 +12,7 @@ import { GetOverviewResponse } from '@kbn/slo-schema/src/rest_specs/routes/get_o import { rulesLocatorID, RulesParams } from '@kbn/observability-plugin/public'; import { useAlertsUrl } from '../../../../hooks/use_alerts_url'; import { useKibana } from '../../../../hooks/use_kibana'; -import { OverViewItem } from './overview_item'; +import { OverviewItem } from './overview_item'; export function SLOOverviewAlerts({ data, @@ -55,7 +55,7 @@ export function SLOOverviewAlerts({ <EuiSpacer size="xs" /> <EuiFlexGroup justifyContent="spaceBetween"> - <OverViewItem + <OverviewItem title={data?.burnRateActiveAlerts} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateActiveAlerts', { defaultMessage: 'Active alerts', @@ -66,7 +66,7 @@ export function SLOOverviewAlerts({ application.navigateToUrl(getAlertsUrl('active')); }} /> - <OverViewItem + <OverviewItem title={data?.burnRateRecoveredAlerts} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateRecoveredAlerts', { defaultMessage: 'Recovered alerts', @@ -77,7 +77,7 @@ export function SLOOverviewAlerts({ application.navigateToUrl(getAlertsUrl('recovered')); }} /> - <OverViewItem + <OverviewItem title={data?.burnRateRules} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateRules', { defaultMessage: 'Rules', diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx index 42c0d199db788..49e9e6e34e0df 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx @@ -20,7 +20,7 @@ import { SLOOverviewAlerts } from './slo_overview_alerts'; import { useGetSettings } from '../../../slo_settings/hooks/use_get_settings'; import { useFetchSLOsOverview } from '../../hooks/use_fetch_slos_overview'; import { useUrlSearchState } from '../../hooks/use_url_search_state'; -import { OverViewItem } from './overview_item'; +import { OverviewItem } from './overview_item'; export function SLOsOverview() { const { state } = useUrlSearchState(); @@ -50,7 +50,7 @@ export function SLOsOverview() { </EuiTitle> <EuiSpacer size="xs" /> <EuiFlexGroup gutterSize="xl" justifyContent="spaceBetween"> - <OverViewItem + <OverviewItem title={data?.healthy} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.healthyLabel', { defaultMessage: 'Healthy', @@ -62,7 +62,7 @@ export function SLOsOverview() { defaultMessage: 'Click to filter SLOs by Healthy status.', })} /> - <OverViewItem + <OverviewItem title={data?.violated} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.violatedLabel', { defaultMessage: 'Violated', @@ -74,7 +74,7 @@ export function SLOsOverview() { defaultMessage: 'Click to filter SLOs by Violated status.', })} /> - <OverViewItem + <OverviewItem title={data?.noData} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.noDataLabel', { defaultMessage: 'No data', @@ -86,7 +86,7 @@ export function SLOsOverview() { defaultMessage: 'Click to filter SLOs by no data status.', })} /> - <OverViewItem + <OverviewItem title={data?.degrading} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.degradingLabel', { defaultMessage: 'Degrading', @@ -98,7 +98,7 @@ export function SLOsOverview() { })} titleColor={theme.colors.warningText} /> - <OverViewItem + <OverviewItem title={data?.stale} description={i18n.translate('xpack.slo.sLOsOverview.euiStat.staleLabel', { defaultMessage: 'Stale', diff --git a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts index 6e3c02a5b921b..ee58618add23c 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts @@ -6,12 +6,11 @@ */ import { CoreSetup, Logger } from '@kbn/core/server'; import { ServerRoute, registerRoutes } from '@kbn/server-route-repository'; -import { ServerRouteCreateOptions } from '@kbn/server-route-repository-utils'; import { SLORequestHandlerContext, SLORoutesDependencies } from './types'; interface RegisterRoutes { core: CoreSetup; - repository: Record<string, ServerRoute<string, any, any, any, ServerRouteCreateOptions>>; + repository: Record<string, ServerRoute<string, any, any, any, any>>; logger: Logger; dependencies: SLORoutesDependencies; isServerless: boolean; diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts index 7f3b395c7adba..2081f38df552f 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts @@ -29,7 +29,6 @@ import { updateSLOParamsSchema, } from '@kbn/slo-schema'; import { getOverviewParamsSchema } from '@kbn/slo-schema/src/rest_specs/routes/get_overview'; -import type { IndicatorTypes } from '../../domain/models'; import { executeWithErrorHandler } from '../../errors'; import { CreateSLO, @@ -60,29 +59,10 @@ import { SloDefinitionClient } from '../../services/slo_definition_client'; import { getSloSettings, storeSloSettings } from '../../services/slo_settings'; import { DefaultSummarySearchClient } from '../../services/summary_search_client'; import { DefaultSummaryTransformGenerator } from '../../services/summary_transform_generator/summary_transform_generator'; -import { - ApmTransactionDurationTransformGenerator, - ApmTransactionErrorRateTransformGenerator, - HistogramTransformGenerator, - KQLCustomTransformGenerator, - MetricCustomTransformGenerator, - SyntheticsAvailabilityTransformGenerator, - TimesliceMetricTransformGenerator, - TransformGenerator, -} from '../../services/transform_generators'; +import { createTransformGenerators } from '../../services/transform_generators'; import { createSloServerRoute } from '../create_slo_server_route'; import { SLORoutesDependencies } from '../types'; -const transformGenerators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(), - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), - 'sli.synthetics.availability': new SyntheticsAvailabilityTransformGenerator(), - 'sli.kql.custom': new KQLCustomTransformGenerator(), - 'sli.metric.custom': new MetricCustomTransformGenerator(), - 'sli.histogram.custom': new HistogramTransformGenerator(), - 'sli.metric.timeslice': new TimesliceMetricTransformGenerator(), -}; - const assertPlatinumLicense = async (plugins: SLORoutesDependencies['plugins']) => { const licensing = await plugins.licensing.start(); const hasCorrectLicense = (await licensing.getLicense()).hasAtLeast('platinum'); @@ -120,14 +100,18 @@ const createSLORoute = createSloServerRoute({ getSpaceId(plugins, request), dataViews.dataViewsServiceFactory(soClient, esClient), ]); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, @@ -168,14 +152,17 @@ const inspectSLORoute = createSloServerRoute({ const soClient = core.savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, @@ -218,14 +205,17 @@ const updateSLORoute = createSloServerRoute({ const soClient = core.savedObjects.client; const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, @@ -271,14 +261,16 @@ const deleteSLORoute = createSloServerRoute({ const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); + const transformGenerators = createTransformGenerators( + spaceId, + dataViewsService, + sloContext.isServerless + ); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, scopedClusterClient, - logger, - spaceId, - dataViewsService, - sloContext.isServerless + logger ); const summaryTransformManager = new DefaultSummaryTransformManager( @@ -346,14 +338,18 @@ const enableSLORoute = createSloServerRoute({ const esClient = core.elasticsearch.client.asCurrentUser; const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, @@ -388,14 +384,17 @@ const disableSLORoute = createSloServerRoute({ const esClient = core.elasticsearch.client.asCurrentUser; const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, @@ -430,14 +429,17 @@ const resetSLORoute = createSloServerRoute({ const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const transformManager = new DefaultTransformManager( - transformGenerators, - scopedClusterClient, - logger, + + const transformGenerators = createTransformGenerators( spaceId, dataViewsService, sloContext.isServerless ); + const transformManager = new DefaultTransformManager( + transformGenerators, + scopedClusterClient, + logger + ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), scopedClusterClient, diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_slos_overview.ts b/x-pack/plugins/observability_solution/slo/server/services/get_slos_overview.ts index 2c1ff46f57ef5..32660b68f4693 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_slos_overview.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_slos_overview.ts @@ -53,19 +53,6 @@ export class GetSLOsOverview { }, body: { aggs: { - worst: { - top_hits: { - sort: { - errorBudgetRemaining: { - order: 'asc', - }, - }, - _source: { - includes: ['sliValue', 'status', 'slo.id', 'slo.instanceId', 'slo.name'], - }, - size: 1, - }, - }, stale: { filter: { range: { @@ -75,31 +62,42 @@ export class GetSLOsOverview { }, }, }, - violated: { + not_stale: { filter: { - term: { - status: 'VIOLATED', + range: { + summaryUpdatedAt: { + gte: `now-${settings.staleThresholdInHours}h`, + }, }, }, - }, - healthy: { - filter: { - term: { - status: 'HEALTHY', + aggs: { + violated: { + filter: { + term: { + status: 'VIOLATED', + }, + }, }, - }, - }, - degrading: { - filter: { - term: { - status: 'DEGRADING', + healthy: { + filter: { + term: { + status: 'HEALTHY', + }, + }, }, - }, - }, - noData: { - filter: { - term: { - status: 'NO_DATA', + degrading: { + filter: { + term: { + status: 'DEGRADING', + }, + }, + }, + noData: { + filter: { + term: { + status: 'NO_DATA', + }, + }, }, }, }, @@ -131,15 +129,11 @@ export class GetSLOsOverview { const aggs = response.aggregations; return { - violated: aggs?.violated.doc_count ?? 0, - degrading: aggs?.degrading.doc_count ?? 0, - healthy: aggs?.healthy.doc_count ?? 0, - noData: aggs?.noData.doc_count ?? 0, + violated: aggs?.not_stale?.violated.doc_count ?? 0, + degrading: aggs?.not_stale?.degrading.doc_count ?? 0, + healthy: aggs?.not_stale?.healthy?.doc_count ?? 0, + noData: aggs?.not_stale?.noData.doc_count ?? 0, stale: aggs?.stale.doc_count ?? 0, - worst: { - value: 0, - id: 'id', - }, burnRateRules: rules.total, burnRateActiveAlerts: alerts.activeAlertCount, burnRateRecoveredAlerts: alerts.recoveredAlertCount, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/synthetics_availability.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/synthetics_availability.test.ts.snap index 3c71844678885..309fdc7ffffb3 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/synthetics_availability.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/synthetics_availability.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Synthetics Availability Transform Generator returns the expected transform params 1`] = ` +exports[`Synthetics Availability Transform Generator when serverless is disabled returns the expected transform params 1`] = ` Object { "_meta": Object { "managed": true, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/transform_generator.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/transform_generator.test.ts.snap index 144a4fa35eda5..7d8e989c1140d 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/transform_generator.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/transform_generator.test.ts.snap @@ -1,8 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Transform Generator builds common runtime mappings and group by with single group by 1`] = `Object {}`; - -exports[`Transform Generator builds common runtime mappings and group by with single group by 2`] = ` +exports[`Transform Generator buildCommonGroupBy builds common groupBy with single group by 1`] = ` Object { "@timestamp": Object { "date_histogram": Object { @@ -18,9 +16,7 @@ Object { } `; -exports[`Transform Generator builds common runtime mappings and group by with single group by 3`] = `Object {}`; - -exports[`Transform Generator builds common runtime mappings and group by with single group by 4`] = ` +exports[`Transform Generator buildCommonGroupBy builds common groupBy with single group by 2`] = ` Object { "@timestamp": Object { "date_histogram": Object { @@ -36,9 +32,7 @@ Object { } `; -exports[`Transform Generator builds common runtime mappings without multi group by 1`] = `Object {}`; - -exports[`Transform Generator builds common runtime mappings without multi group by 2`] = ` +exports[`Transform Generator buildCommonGroupBy builds common groupBy with single group by 3`] = ` Object { "@timestamp": Object { "date_histogram": Object { @@ -59,9 +53,7 @@ Object { } `; -exports[`Transform Generator builds empty runtime mappings without group by 1`] = `Object {}`; - -exports[`Transform Generator builds empty runtime mappings without group by 2`] = ` +exports[`Transform Generator buildCommonGroupBy builds empty runtime mappings without group by 1`] = ` Object { "@timestamp": Object { "date_histogram": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts index b764b83ea9349..c928c121e3e77 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; import { ALL_VALUE } from '@kbn/slo-schema'; import { twoMinute } from '../fixtures/duration'; import { @@ -13,15 +14,14 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { ApmTransactionDurationTransformGenerator } from './apm_transaction_duration'; -import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new ApmTransactionDurationTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new ApmTransactionDurationTransformGenerator(SPACE_ID, dataViewsService); describe('APM Transaction Duration Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator() }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -31,7 +31,7 @@ describe('APM Transaction Duration Transform Generator', () => { id: 'irrelevant', indicator: createAPMTransactionDurationIndicator(), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -46,7 +46,7 @@ describe('APM Transaction Duration Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -60,7 +60,7 @@ describe('APM Transaction Duration Transform Generator', () => { transactionType: ALL_VALUE, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); @@ -72,7 +72,7 @@ describe('APM Transaction Duration Transform Generator', () => { index, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.index).toEqual(index); }); @@ -84,7 +84,7 @@ describe('APM Transaction Duration Transform Generator', () => { filter, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); @@ -99,7 +99,7 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -115,7 +115,7 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -131,7 +131,7 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -147,7 +147,7 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -163,7 +163,7 @@ describe('APM Transaction Duration Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts index b349a5affeceb..d1f05605dab36 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts @@ -8,30 +8,29 @@ import { estypes } from '@elastic/elasticsearch'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { ALL_VALUE, apmTransactionDurationIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; -import { DataViewsService } from '@kbn/data-views-plugin/common'; -import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; +import { TransformGenerator, getElasticsearchQueryOrThrow } from '.'; import { + SLO_DESTINATION_INDEX_NAME, getSLOPipelineId, getSLOTransformId, - SLO_DESTINATION_INDEX_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { APMTransactionDurationIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; -import { parseIndex } from './common'; -import { getTimesliceTargetComparator, getFilterRange } from './common'; +import { getFilterRange, getTimesliceTargetComparator, parseIndex } from './common'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!apmTransactionDurationIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -39,7 +38,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo, slo.indicator), @@ -75,11 +74,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private async buildSource( - slo: SLODefinition, - indicator: APMTransactionDurationIndicator, - dataViewService: DataViewsService - ) { + private async buildSource(slo: SLODefinition, indicator: APMTransactionDurationIndicator) { const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')]; if (indicator.params.service !== ALL_VALUE) { @@ -113,10 +108,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator }, }); } - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.dataViewId, - }); + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); if (!!indicator.params.filter) { queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter, dataView)); @@ -124,7 +116,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts index 13c73443960af..6b71e37ec4b93 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; import { ALL_VALUE } from '@kbn/slo-schema'; import { oneMinute, twoMinute } from '../fixtures/duration'; import { @@ -13,10 +14,9 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate'; -import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new ApmTransactionErrorRateTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new ApmTransactionErrorRateTransformGenerator(SPACE_ID, dataViewsService); describe('APM Transaction Error Rate Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { @@ -24,7 +24,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { id: 'irrelevant', indicator: createAPMTransactionErrorRateIndicator(), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -34,7 +34,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { id: 'irrelevant', indicator: createAPMTransactionErrorRateIndicator(), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -49,7 +49,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform).toMatchSnapshot(); }); @@ -63,7 +63,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { transactionType: ALL_VALUE, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); @@ -75,7 +75,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { index, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.index).toEqual(index); }); @@ -87,7 +87,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { filter, }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); @@ -102,7 +102,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -118,7 +118,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -134,7 +134,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -150,7 +150,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); @@ -166,7 +166,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts index 3aa0d4507e8a4..6adbd1d3eae9f 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts @@ -13,11 +13,11 @@ import { apmTransactionErrorRateIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; -import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; +import { TransformGenerator, getElasticsearchQueryOrThrow } from '.'; import { + SLO_DESTINATION_INDEX_NAME, getSLOPipelineId, getSLOTransformId, - SLO_DESTINATION_INDEX_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { APMTransactionErrorRateIndicator, SLODefinition } from '../../domain/models'; @@ -25,11 +25,11 @@ import { InvalidTransformError } from '../../errors'; import { getFilterRange, getTimesliceTargetComparator, parseIndex } from './common'; export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -37,7 +37,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo), @@ -73,11 +73,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private async buildSource( - slo: SLODefinition, - indicator: APMTransactionErrorRateIndicator, - dataViewService: DataViewsService - ) { + private async buildSource(slo: SLODefinition, indicator: APMTransactionErrorRateIndicator) { const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')]; if (indicator.params.service !== ALL_VALUE) { @@ -112,10 +108,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato }); } - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.dataViewId, - }); + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); if (indicator.params.filter) { queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter, dataView)); @@ -123,7 +116,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts index 2de75b8f7d86c..5410efb048dcd 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts @@ -14,8 +14,8 @@ import { import { HistogramTransformGenerator } from './histogram'; import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new HistogramTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new HistogramTransformGenerator(SPACE_ID, dataViewsService); describe('Histogram Transform Generator', () => { describe('validation', () => { @@ -32,9 +32,7 @@ describe('Histogram Transform Generator', () => { }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL: foo:/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL: foo:/); }); it('throws when the total filter is invalid', async () => { @@ -48,24 +46,20 @@ describe('Histogram Transform Generator', () => { }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL: foo:/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL: foo:/); }); it('throws when the query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: '{ kql.query: invalid' }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createHistogramIndicator() }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -75,7 +69,7 @@ describe('Histogram Transform Generator', () => { id: 'irrelevant', indicator: createHistogramIndicator(), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -90,7 +84,7 @@ describe('Histogram Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -99,7 +93,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); @@ -108,7 +102,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ index: 'my-own-index*' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.index).toBe('my-own-index*'); }); @@ -119,7 +113,7 @@ describe('Histogram Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -130,7 +124,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator(), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -147,7 +141,7 @@ describe('Histogram Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -156,7 +150,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator(), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -171,7 +165,7 @@ describe('Histogram Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -186,7 +180,7 @@ describe('Histogram Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts index b19f9a48e70f0..805e18c9c31db 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts @@ -6,18 +6,17 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { HistogramIndicator, histogramIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; - -import { DataViewsService } from '@kbn/data-views-plugin/common'; -import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; +import { TransformGenerator, getElasticsearchQueryOrThrow, parseIndex } from '.'; import { + SLO_DESTINATION_INDEX_NAME, getSLOPipelineId, getSLOTransformId, - SLO_DESTINATION_INDEX_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { SLODefinition } from '../../domain/models'; @@ -26,11 +25,11 @@ import { GetHistogramIndicatorAggregation } from '../aggregations'; import { getFilterRange, getTimesliceTargetComparator } from './common'; export class HistogramTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!histogramIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -38,7 +37,7 @@ export class HistogramTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -51,19 +50,12 @@ export class HistogramTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private async buildSource( - slo: SLODefinition, - indicator: HistogramIndicator, - dataViewService: DataViewsService - ) { - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.index, - }); + private async buildSource(slo: SLODefinition, indicator: HistogramIndicator) { + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/index.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/index.ts index c58de27e9b98e..9b68c19692ee4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/index.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/index.ts @@ -14,3 +14,4 @@ export * from './metric_custom'; export * from './histogram'; export * from './timeslice_metric'; export * from './common'; +export * from './transform_generators_factory'; diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts index c41e0d4b3df53..e25aefdb3be1a 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts @@ -14,8 +14,8 @@ import { import { KQLCustomTransformGenerator } from './kql_custom'; import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new KQLCustomTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new KQLCustomTransformGenerator(SPACE_ID, dataViewsService); describe('KQL Custom Transform Generator', () => { describe('validation', () => { @@ -23,31 +23,25 @@ describe('KQL Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ good: '{ kql.query: invalid' }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); it('throws when the KQL denominator is invalid', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ total: '{ kql.query: invalid' }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); it('throws when the KQL query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ filter: '{ kql.query: invalid' }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createKQLCustomIndicator() }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -57,7 +51,7 @@ describe('KQL Custom Transform Generator', () => { id: 'irrelevant', indicator: createKQLCustomIndicator(), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -72,7 +66,7 @@ describe('KQL Custom Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -81,7 +75,7 @@ describe('KQL Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); @@ -90,7 +84,7 @@ describe('KQL Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ index: 'my-own-index*' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.index).toBe('my-own-index*'); }); @@ -101,7 +95,7 @@ describe('KQL Custom Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -114,7 +108,7 @@ describe('KQL Custom Transform Generator', () => { good: 'latency < 400 and (http.status_code: 2xx or http.status_code: 3xx or http.status_code: 4xx)', }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -125,7 +119,7 @@ describe('KQL Custom Transform Generator', () => { total: 'http.status_code: *', }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -140,7 +134,7 @@ describe('KQL Custom Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts index 0082e13968c80..61238c82ab600 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts @@ -6,14 +6,13 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { kqlCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; - import { DataViewsService } from '@kbn/data-views-plugin/common'; -import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; +import { kqlCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { TransformGenerator, getElasticsearchQueryOrThrow, parseIndex } from '.'; import { + SLO_DESTINATION_INDEX_NAME, getSLOPipelineId, getSLOTransformId, - SLO_DESTINATION_INDEX_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { KQLCustomIndicator, SLODefinition } from '../../domain/models'; @@ -21,11 +20,11 @@ import { InvalidTransformError } from '../../errors'; import { getFilterRange, getTimesliceTargetComparator } from './common'; export class KQLCustomTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!kqlCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -33,7 +32,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -46,18 +45,11 @@ export class KQLCustomTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private async buildSource( - slo: SLODefinition, - indicator: KQLCustomIndicator, - dataViewService: DataViewsService - ) { - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.dataViewId, - }); + private async buildSource(slo: SLODefinition, indicator: KQLCustomIndicator) { + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts index 85da6de832c98..b725d77af63dc 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts @@ -14,8 +14,8 @@ import { import { MetricCustomTransformGenerator } from './metric_custom'; import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new MetricCustomTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new MetricCustomTransformGenerator(SPACE_ID, dataViewsService); describe('Metric Custom Transform Generator', () => { describe('validation', () => { @@ -28,9 +28,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid equation/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid equation/); }); it('throws when the good filter is invalid', async () => { const anSLO = createSLO({ @@ -41,9 +39,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL: foo:/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL: foo:/); }); it('throws when the total equation is invalid', async () => { const anSLO = createSLO({ @@ -54,9 +50,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid equation/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid equation/); }); it('throws when the total filter is invalid', async () => { const anSLO = createSLO({ @@ -67,23 +61,19 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - await expect(() => - generator.getTransformParams(anSLO, spaceId, dataViewsService) - ).rejects.toThrow(/Invalid KQL: foo:/); + await expect(() => generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL: foo:/); }); it('throws when the query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ filter: '{ kql.query: invalid' }), }); - await expect(() => - generator.getTransformParams(anSLO, spaceId, dataViewsService) - ).rejects.toThrow(/Invalid KQL/); + await expect(() => generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createMetricCustomIndicator() }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -93,7 +83,7 @@ describe('Metric Custom Transform Generator', () => { id: 'irrelevant', indicator: createMetricCustomIndicator(), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -102,7 +92,7 @@ describe('Metric Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); @@ -111,7 +101,7 @@ describe('Metric Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ index: 'my-own-index*' }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.index).toBe('my-own-index*'); }); @@ -122,7 +112,7 @@ describe('Metric Custom Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -138,7 +128,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -152,7 +142,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -168,7 +158,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -182,7 +172,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -196,7 +186,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -210,7 +200,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -224,7 +214,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -239,7 +229,7 @@ describe('Metric Custom Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts index e96f252d5ed84..f2259955bdfb0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts @@ -24,11 +24,11 @@ import { getFilterRange, getTimesliceTargetComparator } from './common'; export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class MetricCustomTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!metricCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -36,7 +36,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -49,18 +49,11 @@ export class MetricCustomTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private async buildSource( - slo: SLODefinition, - indicator: MetricCustomIndicator, - dataViewService: DataViewsService - ) { - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.dataViewId, - }); + private async buildSource(slo: SLODefinition, indicator: MetricCustomIndicator) { + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts index fa40ab9cc1e8d..cccb1c9eba3e3 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts @@ -12,286 +12,330 @@ import { twoMinute } from '../fixtures/duration'; import { createSLO, createSyntheticsAvailabilityIndicator } from '../fixtures/slo'; import { SyntheticsAvailabilityTransformGenerator } from './synthetics_availability'; -const generator = new SyntheticsAvailabilityTransformGenerator(); +const SPACE_ID = 'custom-space'; describe('Synthetics Availability Transform Generator', () => { - const spaceId = 'custom-space'; - - it('returns the expected transform params', async () => { - const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - - expect(transform).toMatchSnapshot(); - expect(transform.source.query?.bool?.filter).toContainEqual({ - term: { - 'summary.final_attempt': true, - }, - }); - }); - - it('groups by config id and observer.name when using default groupings', async () => { - const slo = createSLO({ - id: 'irrelevant', - indicator: createSyntheticsAvailabilityIndicator(), - }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - - expect(transform.pivot?.group_by).toEqual( - expect.objectContaining({ - 'monitor.config_id': { - terms: { - field: 'config_id', - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - }, - }, - }) + describe('when serverless is disabled', () => { + const generator = new SyntheticsAvailabilityTransformGenerator( + SPACE_ID, + dataViewsService, + false ); - }); - it('does not include config id and observer.name when using non default groupings', async () => { - const slo = createSLO({ - id: 'irrelevant', - indicator: createSyntheticsAvailabilityIndicator(), - groupBy: ['host.name'], - }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - - expect(transform.pivot?.group_by).not.toEqual( - expect.objectContaining({ - 'monitor.config_id': { - terms: { - field: 'config_id', - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - }, - }, - }) - ); + it('returns the expected transform params', async () => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + }); + const transform = await generator.getTransformParams(slo); - expect(transform.pivot?.group_by).toEqual( - expect.objectContaining({ - 'slo.groupings.host.name': { - terms: { - field: 'host.name', - }, + expect(transform).toMatchSnapshot(); + expect(transform.source.query?.bool?.filter).toContainEqual({ + term: { + 'summary.final_attempt': true, }, - }) - ); - }); + }); + }); - it.each([[[]], [[ALL_VALUE]]])( - 'adds observer.geo.name and monitor.name to groupings key by default, multi group by', - async (groupBy) => { + it('groups by config id and observer.name when using default groupings', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), - groupBy, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); + const transform = await generator.getTransformParams(slo); expect(transform.pivot?.group_by).toEqual( expect.objectContaining({ - 'slo.groupings.monitor.name': { + 'monitor.config_id': { terms: { - field: 'monitor.name', + field: 'config_id', }, }, - 'slo.groupings.observer.geo.name': { + 'observer.name': { terms: { - field: 'observer.geo.name', + field: 'observer.name', }, }, }) ); - } - ); + }); - it.each([[''], [ALL_VALUE]])( - 'adds observer.geo.name and monitor.name to groupings key by default, single group by', - async (groupBy) => { + it('does not include config id and observer.name when using non default groupings', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), - groupBy, + groupBy: ['host.name'], }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); + const transform = await generator.getTransformParams(slo); - expect(transform.pivot?.group_by).toEqual( + expect(transform.pivot?.group_by).not.toEqual( expect.objectContaining({ - 'slo.groupings.monitor.name': { + 'monitor.config_id': { terms: { - field: 'monitor.name', + field: 'config_id', }, }, - 'slo.groupings.observer.geo.name': { + 'observer.name': { terms: { - field: 'observer.geo.name', + field: 'observer.name', }, }, }) ); - } - ); - - it.each([['host.name'], [['host.name']]])('handles custom groupBy', async (groupBy) => { - const slo = createSLO({ - id: 'irrelevant', - indicator: createSyntheticsAvailabilityIndicator(), - groupBy, - }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - expect(transform.pivot?.group_by).toEqual( - expect.objectContaining({ - 'slo.groupings.host.name': { - terms: { - field: 'host.name', + expect(transform.pivot?.group_by).toEqual( + expect.objectContaining({ + 'slo.groupings.host.name': { + terms: { + field: 'host.name', + }, }, - }, - }) + }) + ); + }); + + it.each([[[]], [[ALL_VALUE]]])( + 'adds observer.geo.name and monitor.name to groupings key by default, multi group by', + async (groupBy) => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + groupBy, + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.pivot?.group_by).toEqual( + expect.objectContaining({ + 'slo.groupings.monitor.name': { + terms: { + field: 'monitor.name', + }, + }, + 'slo.groupings.observer.geo.name': { + terms: { + field: 'observer.geo.name', + }, + }, + }) + ); + } ); - }); - it('filters by summary.final_attempt', async () => { - const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); + it.each([[''], [ALL_VALUE]])( + 'adds observer.geo.name and monitor.name to groupings key by default, single group by', + async (groupBy) => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + groupBy, + }); + const transform = await generator.getTransformParams(slo); - expect(transform.source.query?.bool?.filter).toContainEqual({ - term: { - 'summary.final_attempt': true, - }, - }); - }); + expect(transform.pivot?.group_by).toEqual( + expect.objectContaining({ + 'slo.groupings.monitor.name': { + terms: { + field: 'monitor.name', + }, + }, + 'slo.groupings.observer.geo.name': { + terms: { + field: 'observer.geo.name', + }, + }, + }) + ); + } + ); + + it.each([[['host.name']], [['host.name', 'host.region']]])( + 'handles custom groupBy', + async (groupBy) => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + groupBy, + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.pivot?.group_by).toEqual( + expect.objectContaining({ + 'slo.groupings.host.name': { + terms: { + field: 'host.name', + }, + }, + }) + ); + } + ); - it('adds tag filters', async () => { - const tags = [ - { value: 'tag-1', label: 'tag1' }, - { value: 'tag-2', label: 'tag2' }, - ]; - const indicator = createSyntheticsAvailabilityIndicator(); - const slo = createSLO({ - id: 'irrelevant', - indicator: { - ...indicator, - params: { - ...indicator.params, - tags, + it('filters by summary.final_attempt', async () => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.source.query?.bool?.filter).toContainEqual({ + term: { + 'summary.final_attempt': true, }, - } as SLODefinition['indicator'], + }); }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - expect(transform.source.query?.bool?.filter).toContainEqual({ - terms: { - tags: ['tag-1', 'tag-2'], - }, - }); - expect(transform.pivot?.group_by?.tags).toEqual({ - terms: { - field: 'tags', - }, - }); - }); + it('adds tag filters', async () => { + const tags = [ + { value: 'tag-1', label: 'tag1' }, + { value: 'tag-2', label: 'tag2' }, + ]; + const indicator = createSyntheticsAvailabilityIndicator(); + const slo = createSLO({ + id: 'irrelevant', + indicator: { + ...indicator, + params: { + ...indicator.params, + tags, + }, + } as SLODefinition['indicator'], + }); + const transform = await generator.getTransformParams(slo); - it('adds monitorId filter', async () => { - const monitorIds = [ - { value: 'id-1', label: 'Monitor name 1' }, - { value: 'id-2', label: 'Monitor name 2' }, - ]; - const indicator = createSyntheticsAvailabilityIndicator(); - const slo = createSLO({ - id: 'irrelevant', - indicator: { - ...indicator, - params: { - ...indicator.params, - monitorIds, + expect(transform.source.query?.bool?.filter).toContainEqual({ + terms: { + tags: ['tag-1', 'tag-2'], }, - } as SLODefinition['indicator'], + }); + expect(transform.pivot?.group_by?.tags).toEqual({ + terms: { + field: 'tags', + }, + }); }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - expect(transform.source.query?.bool?.filter).toContainEqual({ - terms: { - 'monitor.id': ['id-1', 'id-2'], - }, - }); - expect(transform.pivot?.group_by?.['monitor.id']).toEqual({ - terms: { - field: 'monitor.id', - }, - }); - }); + it('adds monitorId filter', async () => { + const monitorIds = [ + { value: 'id-1', label: 'Monitor name 1' }, + { value: 'id-2', label: 'Monitor name 2' }, + ]; + const indicator = createSyntheticsAvailabilityIndicator(); + const slo = createSLO({ + id: 'irrelevant', + indicator: { + ...indicator, + params: { + ...indicator.params, + monitorIds, + }, + } as SLODefinition['indicator'], + }); + const transform = await generator.getTransformParams(slo); - it('adds project id filter', async () => { - const projects = [ - { value: 'id-1', label: 'Project name 1' }, - { value: 'id-2', label: 'Project name 2' }, - ]; - const indicator = createSyntheticsAvailabilityIndicator(); - const slo = createSLO({ - id: 'irrelevant', - indicator: { - ...indicator, - params: { - ...indicator.params, - projects, + expect(transform.source.query?.bool?.filter).toContainEqual({ + terms: { + 'monitor.id': ['id-1', 'id-2'], + }, + }); + expect(transform.pivot?.group_by?.['monitor.id']).toEqual({ + terms: { + field: 'monitor.id', }, - } as SLODefinition['indicator'], + }); }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); - expect(transform.source.query?.bool?.filter).toContainEqual({ - terms: { - 'monitor.project.id': ['id-1', 'id-2'], - }, + it('adds project id filter', async () => { + const projects = [ + { value: 'id-1', label: 'Project name 1' }, + { value: 'id-2', label: 'Project name 2' }, + ]; + const indicator = createSyntheticsAvailabilityIndicator(); + const slo = createSLO({ + id: 'irrelevant', + indicator: { + ...indicator, + params: { + ...indicator.params, + projects, + }, + } as SLODefinition['indicator'], + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.source.query?.bool?.filter).toContainEqual({ + terms: { + 'monitor.project.id': ['id-1', 'id-2'], + }, + }); + expect(transform.pivot?.group_by?.['monitor.project.id']).toEqual({ + terms: { + field: 'monitor.project.id', + }, + }); }); - expect(transform.pivot?.group_by?.['monitor.project.id']).toEqual({ - terms: { - field: 'monitor.project.id', - }, + + it('filters by space', async () => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createSyntheticsAvailabilityIndicator(), + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.source.query?.bool?.filter).toContainEqual({ + term: { + 'meta.space_id': SPACE_ID, + }, + }); }); - }); - it('filters by space', async () => { - const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService, false); + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { + const slo = createSLO({ + indicator: createSyntheticsAvailabilityIndicator(), + settings: { + frequency: twoMinute(), + syncDelay: twoMinute(), + preventInitialBackfill: true, + }, + }); + + const transform = await generator.getTransformParams(slo); + + // @ts-ignore + const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); - expect(transform.source.query?.bool?.filter).toContainEqual({ - term: { - 'meta.space_id': spaceId, - }, + expect(rangeFilter).toEqual({ + range: { + '@timestamp': { + gte: 'now-300s/m', // 2m + 2m + 60s + }, + }, + }); }); - }); - it("overrides the range filter when 'preventInitialBackfill' is true", async () => { - const slo = createSLO({ - indicator: createSyntheticsAvailabilityIndicator(), - settings: { - frequency: twoMinute(), - syncDelay: twoMinute(), - preventInitialBackfill: true, - }, + it("uses the 'event.ingested' as syncField", async () => { + const slo = createSLO({ + indicator: createSyntheticsAvailabilityIndicator(), + }); + const transform = await generator.getTransformParams(slo); + + expect(transform.sync?.time?.field).toEqual('event.ingested'); }); + }); - const transform = await generator.getTransformParams(slo, 'default', dataViewsService, false); + describe('when serverless is enabled', () => { + const generator = new SyntheticsAvailabilityTransformGenerator( + SPACE_ID, + dataViewsService, + true + ); - // @ts-ignore - const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); + it("overrides the syncField with '@timestamp'", async () => { + const slo = createSLO({ + indicator: createSyntheticsAvailabilityIndicator(), + }); + const transform = await generator.getTransformParams(slo); - expect(rangeFilter).toEqual({ - range: { - '@timestamp': { - gte: 'now-300s/m', // 2m + 2m + 60s - }, - }, + expect(transform.sync?.time?.field).toEqual('@timestamp'); }); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts index 285820f908182..65fda9c3fc222 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts @@ -28,12 +28,11 @@ import { InvalidTransformError } from '../../errors'; import { getFilterRange } from './common'; export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService, - isServerless: boolean - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService, isServerless: boolean) { + super(spaceId, dataViewService, isServerless); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!syntheticsAvailabilityIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -41,11 +40,11 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, spaceId, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo), - this.buildSettings(slo, isServerless ? '@timestamp' : 'event.ingested'), + this.buildSettings(slo, this.isServerless ? '@timestamp' : 'event.ingested'), slo ); } @@ -108,15 +107,10 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator ); } - private async buildSource( - slo: SLODefinition, - indicator: SyntheticsAvailabilityIndicator, - spaceId: string, - dataViewService: DataViewsService - ) { + private async buildSource(slo: SLODefinition, indicator: SyntheticsAvailabilityIndicator) { const queryFilter: estypes.QueryDslQueryContainer[] = [ { term: { 'summary.final_attempt': true } }, - { term: { 'meta.space_id': spaceId } }, + { term: { 'meta.space_id': this.spaceId } }, getFilterRange(slo, '@timestamp'), ]; const { monitorIds, tags, projects } = buildParamValues({ @@ -153,14 +147,11 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter)); } - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.dataViewId, - }); + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); return { index: SYNTHETICS_INDEX_PATTERN, - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: queryFilter, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts index 02c69f38d6705..c8fd371479c29 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts @@ -5,18 +5,17 @@ * 2.0. */ +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; import { twoMinute } from '../fixtures/duration'; import { - createTimesliceMetricIndicator, - createSLOWithTimeslicesBudgetingMethod, createSLO, + createSLOWithTimeslicesBudgetingMethod, + createTimesliceMetricIndicator, } from '../fixtures/slo'; import { TimesliceMetricTransformGenerator } from './timeslice_metric'; -import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; - -const generator = new TimesliceMetricTransformGenerator(); -const spaceId = 'custom-space'; +const SPACE_ID = 'custom-space'; +const generator = new TimesliceMetricTransformGenerator(SPACE_ID, dataViewsService); const everythingIndicator = createTimesliceMetricIndicator( [ { name: 'A', aggregation: 'avg', field: 'test.field', filter: 'test.category: "test"' }, @@ -38,7 +37,7 @@ describe('Timeslice Metric Transform Generator', () => { '(A / 200) + A' ), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + await expect(generator.getTransformParams(anSLO)).rejects.toThrow( 'The sli.metric.timeslice indicator MUST have a timeslice budgeting method.' ); }); @@ -49,9 +48,7 @@ describe('Timeslice Metric Transform Generator', () => { '(a / 200) + A' ), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid equation/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid equation/); }); it('throws when the metric filter is invalid', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ @@ -60,9 +57,7 @@ describe('Timeslice Metric Transform Generator', () => { '(A / 200) + A' ), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL: test:/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL: test:/); }); it('throws when the query_filter is invalid', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ @@ -72,9 +67,7 @@ describe('Timeslice Metric Transform Generator', () => { 'test:' ), }); - await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( - /Invalid KQL/ - ); + await expect(generator.getTransformParams(anSLO)).rejects.toThrow(/Invalid KQL/); }); }); @@ -83,7 +76,7 @@ describe('Timeslice Metric Transform Generator', () => { id: 'irrelevant', indicator: everythingIndicator, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -93,7 +86,7 @@ describe('Timeslice Metric Transform Generator', () => { id: 'irrelevant', indicator: everythingIndicator, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot(); }); @@ -102,7 +95,7 @@ describe('Timeslice Metric Transform Generator', () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: everythingIndicator, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); @@ -114,7 +107,7 @@ describe('Timeslice Metric Transform Generator', () => { params: { ...everythingIndicator.params, index: 'my-own-index*' }, }, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.source.index).toBe('my-own-index*'); }); @@ -126,7 +119,7 @@ describe('Timeslice Metric Transform Generator', () => { params: { ...everythingIndicator.params, timestampField: 'my-date-field' }, }, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -137,7 +130,7 @@ describe('Timeslice Metric Transform Generator', () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: everythingIndicator, }); - const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); + const transform = await generator.getTransformParams(anSLO); expect(transform.pivot!.aggregations!._metric).toEqual({ bucket_script: { @@ -185,7 +178,7 @@ describe('Timeslice Metric Transform Generator', () => { }, }); - const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); + const transform = await generator.getTransformParams(slo); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts index b0e32f7094a2f..e2f305e68fee8 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts @@ -28,11 +28,11 @@ import { getFilterRange } from './common'; const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class TimesliceMetricTransformGenerator extends TransformGenerator { - public async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + public async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { if (!timesliceMetricIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -40,7 +40,7 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - await this.buildSource(slo, slo.indicator, dataViewService), + await this.buildSource(slo, slo.indicator), this.buildDestination(slo), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -53,18 +53,12 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private async buildSource( - slo: SLODefinition, - indicator: TimesliceMetricIndicator, - dataViewService: DataViewsService - ) { - const dataView = await this.getIndicatorDataView({ - dataViewService, - dataViewId: indicator.params.index, - }); + private async buildSource(slo: SLODefinition, indicator: TimesliceMetricIndicator) { + const dataView = await this.getIndicatorDataView(indicator.params.dataViewId); + return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), + runtime_mappings: this.buildCommonRuntimeMappings(dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.test.ts index 9f07c6cfb5afa..e70d406d75396 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.test.ts @@ -7,50 +7,42 @@ import { createAPMTransactionErrorRateIndicator, createSLO } from '../fixtures/slo'; import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; -const generator = new ApmTransactionErrorRateTransformGenerator(); +const generator = new ApmTransactionErrorRateTransformGenerator('my-space-id', dataViewsService); describe('Transform Generator', () => { - it('builds empty runtime mappings without group by', async () => { - const slo = createSLO({ - id: 'irrelevant', - indicator: createAPMTransactionErrorRateIndicator(), - }); - const commonRuntime = generator.buildCommonRuntimeMappings(slo); - expect(commonRuntime).toMatchSnapshot(); - - const commonGroupBy = generator.buildCommonGroupBy(slo); - expect(commonGroupBy).toMatchSnapshot(); - }); - - it.each(['example', ['example']])( - 'builds common runtime mappings and group by with single group by', - async (groupBy) => { - const indicator = createAPMTransactionErrorRateIndicator(); + describe('buildCommonGroupBy', () => { + it('builds empty runtime mappings without group by', async () => { const slo = createSLO({ id: 'irrelevant', - groupBy, - indicator, + indicator: createAPMTransactionErrorRateIndicator(), }); - const commonRuntime = generator.buildCommonRuntimeMappings(slo); - expect(commonRuntime).toMatchSnapshot(); const commonGroupBy = generator.buildCommonGroupBy(slo); expect(commonGroupBy).toMatchSnapshot(); - } - ); - - it('builds common runtime mappings without multi group by', async () => { - const indicator = createAPMTransactionErrorRateIndicator(); - const slo = createSLO({ - id: 'irrelevant', - groupBy: ['example1', 'example2'], - indicator, }); - const commonRuntime = generator.buildCommonRuntimeMappings(slo); - expect(commonRuntime).toMatchSnapshot(); - const commonGroupBy = generator.buildCommonGroupBy(slo); - expect(commonGroupBy).toMatchSnapshot(); + it.each(['example', ['example'], ['example1', 'example2']])( + 'builds common groupBy with single group by', + async (groupBy) => { + const indicator = createAPMTransactionErrorRateIndicator(); + const slo = createSLO({ + id: 'irrelevant', + groupBy, + indicator, + }); + + const commonGroupBy = generator.buildCommonGroupBy(slo); + expect(commonGroupBy).toMatchSnapshot(); + } + ); + }); + + describe('buildCommonRuntimeMappings', () => { + it('builds empty runtime mappings without data view', async () => { + const runtimeMappings = generator.buildCommonRuntimeMappings(); + expect(runtimeMappings).toEqual({}); + }); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts index 25b0dc161661c..6c44471fd6566 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts @@ -9,23 +9,22 @@ import { MappingRuntimeFields, TransformPutTransformRequest, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ALL_VALUE, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; import { DataView, DataViewsService } from '@kbn/data-views-plugin/common'; +import { ALL_VALUE, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; import { TransformSettings } from '../../assets/transform_templates/slo_transform_template'; import { SLODefinition } from '../../domain/models'; export abstract class TransformGenerator { - public abstract getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService, - isServerless: boolean - ): Promise<TransformPutTransformRequest>; + constructor( + protected spaceId: string, + protected dataViewService: DataViewsService, + protected isServerless: boolean = false + ) {} - public buildCommonRuntimeMappings(slo: SLODefinition, dataView?: DataView): MappingRuntimeFields { - return { - ...(dataView?.getRuntimeMappings?.() ?? {}), - }; + public abstract getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest>; + + public buildCommonRuntimeMappings(dataView?: DataView): MappingRuntimeFields { + return dataView?.getRuntimeMappings?.() ?? {}; } public buildDescription(slo: SLODefinition): string { @@ -71,17 +70,11 @@ export abstract class TransformGenerator { }; } - public async getIndicatorDataView({ - dataViewService, - dataViewId, - }: { - dataViewService: DataViewsService; - dataViewId?: string; - }): Promise<DataView | undefined> { + public async getIndicatorDataView(dataViewId?: string): Promise<DataView | undefined> { let dataView: DataView | undefined; if (dataViewId) { try { - dataView = await dataViewService.get(dataViewId); + dataView = await this.dataViewService.get(dataViewId); } catch (e) { // If the data view is not found, we will continue without it } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generators_factory.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generators_factory.ts new file mode 100644 index 0000000000000..1da2ce1eca4e5 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generators_factory.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataViewsService } from '@kbn/data-views-plugin/server'; +import { + ApmTransactionDurationTransformGenerator, + ApmTransactionErrorRateTransformGenerator, + HistogramTransformGenerator, + KQLCustomTransformGenerator, + MetricCustomTransformGenerator, + SyntheticsAvailabilityTransformGenerator, + TimesliceMetricTransformGenerator, + TransformGenerator, +} from '.'; +import { IndicatorTypes } from '../../domain/models'; + +export function createTransformGenerators( + spaceId: string, + dataViewsService: DataViewsService, + isServerless: boolean +): Record<IndicatorTypes, TransformGenerator> { + return { + 'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator( + spaceId, + dataViewsService + ), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), + 'sli.synthetics.availability': new SyntheticsAvailabilityTransformGenerator( + spaceId, + dataViewsService, + isServerless + ), + 'sli.kql.custom': new KQLCustomTransformGenerator(spaceId, dataViewsService), + 'sli.metric.custom': new MetricCustomTransformGenerator(spaceId, dataViewsService), + 'sli.histogram.custom': new HistogramTransformGenerator(spaceId, dataViewsService), + 'sli.metric.timeslice': new TimesliceMetricTransformGenerator(spaceId, dataViewsService), + }; +} diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts index e837db4e88dc2..aa0884860e8e0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts @@ -44,15 +44,12 @@ describe('TransformManager', () => { it('throws when no generator exists for the slo indicator type', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionDuration': new DummyTransformGenerator(), + 'sli.apm.transactionDuration': new DummyTransformGenerator(spaceId, dataViewsService), }; const service = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await expect( @@ -63,15 +60,12 @@ describe('TransformManager', () => { it('throws when transform generator fails', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionDuration': new FailTransformGenerator(), + 'sli.apm.transactionDuration': new FailTransformGenerator(spaceId, dataViewsService), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await expect( @@ -85,15 +79,15 @@ describe('TransformManager', () => { it('installs the transform', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); @@ -110,15 +104,15 @@ describe('TransformManager', () => { it('previews the transform', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await transformManager.preview('slo-transform-id'); @@ -133,15 +127,15 @@ describe('TransformManager', () => { it('starts the transform', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await transformManager.start('slo-transform-id'); @@ -156,15 +150,15 @@ describe('TransformManager', () => { it('stops the transform', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await transformManager.stop('slo-transform-id'); @@ -179,15 +173,15 @@ describe('TransformManager', () => { it('uninstalls the transform', async () => { // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await transformManager.uninstall('slo-transform-id'); @@ -203,15 +197,15 @@ describe('TransformManager', () => { ); // @ts-ignore defining only a subset of the possible SLI const generators: Record<IndicatorTypes, TransformGenerator> = { - 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator( + spaceId, + dataViewsService + ), }; const transformManager = new DefaultTransformManager( generators, scopedClusterClientMock, - loggerMock, - spaceId, - dataViewsService, - false + loggerMock ); await transformManager.uninstall('slo-transform-id'); @@ -224,21 +218,20 @@ describe('TransformManager', () => { }); class DummyTransformGenerator extends TransformGenerator { - async getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + async getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { return {} as TransformPutTransformRequest; } } class FailTransformGenerator extends TransformGenerator { - getTransformParams( - slo: SLODefinition, - spaceId: string, - dataViewService: DataViewsService - ): Promise<TransformPutTransformRequest> { + constructor(spaceId: string, dataViewService: DataViewsService) { + super(spaceId, dataViewService); + } + + getTransformParams(slo: SLODefinition): Promise<TransformPutTransformRequest> { throw new Error('Some error'); } } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts index aed9931822bdc..464d1f1aeaa59 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { IScopedClusterClient, Logger } from '@kbn/core/server'; - import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataViewsService } from '@kbn/data-views-plugin/server'; -import { SLODefinition, IndicatorTypes } from '../domain/models'; +import { IScopedClusterClient, Logger } from '@kbn/core/server'; +import { IndicatorTypes, SLODefinition } from '../domain/models'; import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { TransformGenerator } from './transform_generators'; @@ -29,10 +27,7 @@ export class DefaultTransformManager implements TransformManager { constructor( private generators: Record<IndicatorTypes, TransformGenerator>, private scopedClusterClient: IScopedClusterClient, - private logger: Logger, - private spaceId: string, - private dataViewService: DataViewsService, - private isServerless: boolean + private logger: Logger ) {} async install(slo: SLODefinition): Promise<TransformId> { @@ -42,12 +37,7 @@ export class DefaultTransformManager implements TransformManager { throw new Error(`Unsupported indicator type [${slo.indicator.type}]`); } - const transformParams = await generator.getTransformParams( - slo, - this.spaceId, - this.dataViewService, - this.isServerless - ); + const transformParams = await generator.getTransformParams(slo); try { await retryTransientEsErrors( () => this.scopedClusterClient.asSecondaryAuthUser.transform.putTransform(transformParams), @@ -74,12 +64,7 @@ export class DefaultTransformManager implements TransformManager { throw new Error(`Unsupported indicator type [${slo.indicator.type}]`); } - return await generator.getTransformParams( - slo, - this.spaceId, - this.dataViewService, - this.isServerless - ); + return await generator.getTransformParams(slo); } async preview(transformId: string): Promise<void> { diff --git a/x-pack/plugins/observability_solution/slo/tsconfig.json b/x-pack/plugins/observability_solution/slo/tsconfig.json index be74e370a1fc1..001c835fcb5cb 100644 --- a/x-pack/plugins/observability_solution/slo/tsconfig.json +++ b/x-pack/plugins/observability_solution/slo/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/observability-plugin", "@kbn/observability-shared-plugin", "@kbn/kibana-react-plugin", - "@kbn/react-kibana-context-theme", "@kbn/shared-ux-link-redirect-app", "@kbn/kibana-utils-plugin", "@kbn/slo-schema", @@ -100,6 +99,5 @@ "@kbn/observability-alerting-rule-utils", "@kbn/discover-shared-plugin", "@kbn/server-route-repository-client", - "@kbn/server-route-repository-utils" ] } diff --git a/x-pack/plugins/observability_solution/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts b/x-pack/plugins/observability_solution/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts index f4e733baf6ef7..025481c6b879b 100644 --- a/x-pack/plugins/observability_solution/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts +++ b/x-pack/plugins/observability_solution/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { mockLayerList } from './__mocks__/regions_layer.mock'; import { useLayerList } from './use_layer_list'; diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts index a517affac54e6..d6b0f31ddc5f3 100644 --- a/x-pack/plugins/reporting/server/config/index.test.ts +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -54,19 +54,8 @@ describe('deprecations', () => { `); }); - it('logs a warning if csv.enablePanelActionDownload: true is set', () => { - const { messages } = applyReportingDeprecations({ csv: { enablePanelActionDownload: true } }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "The default mechanism for Reporting privileges will work differently in future versions, which will affect the behavior of this cluster. Set \\"xpack.reporting.roles.enabled\\" to \\"false\\" to adopt the future behavior before upgrading.", - "The \\"xpack.reporting.csv.enablePanelActionDownload\\" setting is deprecated.", - ] - `); - }); - it('does not log a warning recommended settings are used', () => { const { messages } = applyReportingDeprecations({ - csv: { enablePanelActionDownload: false }, roles: { enabled: false }, }); expect(messages).toMatchInlineSnapshot(`Array []`); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 709d072a9035a..c99e2667eb6df 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -13,10 +13,7 @@ import { ConfigSchema, ReportingConfigType } from '@kbn/reporting-server'; export const config: PluginConfigDescriptor<ReportingConfigType> = { exposeToBrowser: { - csv: { - enablePanelActionDownload: true, - scroll: true, - }, + csv: { scroll: true }, poll: true, roles: true, export_types: true, @@ -69,44 +66,11 @@ export const config: PluginConfigDescriptor<ReportingConfigType> = { }, }); } - - if (reporting?.csv?.enablePanelActionDownload === true) { - addDeprecation({ - configPath: `${fromPath}.csv.enablePanelActionDownload`, - title: i18n.translate('xpack.reporting.deprecations.csvPanelActionDownload.title', { - defaultMessage: - 'The setting to enable CSV Download from saved search panels in dashboards is deprecated.', - }), - level: 'warning', - message: i18n.translate('xpack.reporting.deprecations.csvPanelActionDownload.message', { - defaultMessage: `The "{enablePanelActionDownload}" setting is deprecated.`, - values: { - enablePanelActionDownload: `${fromPath}.csv.enablePanelActionDownload`, - }, - }), - correctiveActions: { - manualSteps: [ - i18n.translate('xpack.reporting.deprecations.csvPanelActionDownload.manualStep1', { - defaultMessage: - 'Remove "{enablePanelActionDownload}" from `kibana.yml` or change the setting to `false`.', - values: { - enablePanelActionDownload: `${fromPath}.csv.enablePanelActionDownload`, - }, - }), - i18n.translate('xpack.reporting.deprecations.csvPanelActionDownload.manualStep2', { - defaultMessage: - 'Use the replacement panel action to generate CSV reports from saved search panels in the Dashboard application.', - }), - ], - }, - }); - } }, ], exposeToUsage: { capture: { maxAttempts: true }, csv: { - enablePanelActionDownload: true, maxSizeBytes: true, scroll: { size: true, duration: true }, }, diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx index cdd5f12408480..9df28ccec4bd6 100644 --- a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx @@ -18,6 +18,7 @@ import { EuiText, useEuiTheme, useGeneratedHtmlId, + EuiIconTip, } from '@elastic/eui'; interface BaseQuickStatProps { @@ -33,6 +34,7 @@ interface BaseQuickStatProps { }>; setOpen: (open: boolean) => void; first?: boolean; + tooltipContent?: string; } export const QuickStat: React.FC<BaseQuickStatProps> = ({ @@ -45,6 +47,7 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({ secondaryTitle, iconColor, content, + tooltipContent, ...rest }) => { const { euiTheme } = useEuiTheme(); @@ -93,6 +96,11 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({ {secondaryTitle} </EuiText> </EuiFlexItem> + {tooltipContent && ( + <EuiFlexItem> + <EuiIconTip content={tooltipContent} /> + </EuiFlexItem> + )} </EuiFlexGroup> </EuiPanel> } diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx index e051fee17d2e2..de1ebe0a8dccf 100644 --- a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx @@ -28,6 +28,7 @@ export interface QuickStatsProps { index: Index; mappings: Mappings; indexDocuments: IndexDocuments; + tooltipContent?: string; } export const SetupAISearchButton: React.FC = () => { @@ -107,6 +108,10 @@ export const QuickStats: React.FC<QuickStatsProps> = ({ index, mappings, indexDo description: index.size ?? '0b', }, ]} + tooltipContent={i18n.translate('xpack.searchIndices.quickStats.documentCountTooltip', { + defaultMessage: + 'This excludes nested documents, which Elasticsearch uses internally to store chunks of vectors.', + })} first /> </EuiFlexItem> diff --git a/x-pack/plugins/search_inference_endpoints/common/doc_links.ts b/x-pack/plugins/search_inference_endpoints/common/doc_links.ts index bb426180a36e8..483803e970d82 100644 --- a/x-pack/plugins/search_inference_endpoints/common/doc_links.ts +++ b/x-pack/plugins/search_inference_endpoints/common/doc_links.ts @@ -15,7 +15,7 @@ class InferenceEndpointsDocLinks { constructor() {} setDocLinks(newDocLinks: DocLinks) { - this.createInferenceEndpoint = newDocLinks.enterpriseSearch.inferenceApiCreate; + this.createInferenceEndpoint = newDocLinks.inferenceManagement.inferenceAPIDocumentation; this.semanticSearchElser = newDocLinks.enterpriseSearch.elser; this.semanticSearchE5 = newDocLinks.enterpriseSearch.e5Model; } diff --git a/x-pack/plugins/search_inference_endpoints/common/translations.ts b/x-pack/plugins/search_inference_endpoints/common/translations.ts index 0f1aa4a8abcfc..8b63725c59f96 100644 --- a/x-pack/plugins/search_inference_endpoints/common/translations.ts +++ b/x-pack/plugins/search_inference_endpoints/common/translations.ts @@ -26,49 +26,6 @@ export const MANAGE_INFERENCE_ENDPOINTS_LABEL = i18n.translate( } ); -export const CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription', - { - defaultMessage: - "Inference endpoints enable you to perform inference tasks using NLP models provided by third-party services or Elastic's built-in models like ELSER and E5. Set up tasks such as text embedding, completions, reranking, and more by using the Create Inference API.", - } -); - -export const START_WITH_PREPARED_ENDPOINTS_LABEL = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel', - { - defaultMessage: 'Learn more about built-in NLP models:', - } -); - -export const ELSER_TITLE = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.elserTitle', - { - defaultMessage: 'ELSER', - } -); - -export const LEARN_HOW_TO_CREATE_INFERENCE_ENDPOINTS_LINK = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.learnHowToCreateInferenceEndpoints', - { - defaultMessage: 'Learn how to create inference endpoints', - } -); - -export const SEMANTIC_SEARCH_WITH_ELSER_LINK = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithElser', - { - defaultMessage: 'Semantic search with ELSER', - } -); - -export const SEMANTIC_SEARCH_WITH_E5_LINK = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithE5', - { - defaultMessage: 'Semantic search with E5 Multilingual', - } -); - export const VIEW_YOUR_MODELS_LINK = i18n.translate( 'xpack.searchInferenceEndpoints.viewYourModels', { @@ -83,25 +40,6 @@ export const API_DOCUMENTATION_LINK = i18n.translate( } ); -export const ELSER_DESCRIPTION = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription', - { - defaultMessage: "ELSER is Elastic's sparse vector NLP model for semantic search in English.", - } -); - -export const E5_TITLE = i18n.translate('xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title', { - defaultMessage: 'E5 Multilingual', -}); - -export const E5_DESCRIPTION = i18n.translate( - 'xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description', - { - defaultMessage: - 'E5 is a third-party NLP model that enables you to perform multilingual semantic search by using dense vector representations.', - } -); - export const ERROR_TITLE = i18n.translate('xpack.searchInferenceEndpoints.inferenceId.errorTitle', { defaultMessage: 'Error adding inference endpoint', }); diff --git a/x-pack/plugins/search_inference_endpoints/kibana.jsonc b/x-pack/plugins/search_inference_endpoints/kibana.jsonc index f535a9df27e99..25b7b391b955a 100644 --- a/x-pack/plugins/search_inference_endpoints/kibana.jsonc +++ b/x-pack/plugins/search_inference_endpoints/kibana.jsonc @@ -24,6 +24,7 @@ "optionalPlugins": [ "cloud", "console", + "serverless" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts index 7ce1e578f1db0..931994c46afca 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts @@ -34,3 +34,4 @@ export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AllInferenceEndpointsTable }; export const PIPELINE_URL = 'ingest/ingest_pipelines'; +export const SERVERLESS_INDEX_MANAGEMENT_URL = 'index_details'; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx index 790bb5ec09913..371b204e0acd6 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx @@ -33,6 +33,7 @@ interface UseFilterParams { options: MultiSelectFilterOption[]; renderOption?: (option: MultiSelectFilterOption) => React.ReactNode; selectedOptionKeys?: string[]; + dataTestSubj?: string; } export const MultiSelectFilter: React.FC<UseFilterParams> = ({ @@ -41,6 +42,7 @@ export const MultiSelectFilter: React.FC<UseFilterParams> = ({ options: rawOptions, selectedOptionKeys = [], renderOption, + dataTestSubj, }) => { const { euiTheme } = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -55,7 +57,7 @@ export const MultiSelectFilter: React.FC<UseFilterParams> = ({ ); return ( - <EuiFilterGroup> + <EuiFilterGroup data-test-subj={dataTestSubj}> <EuiPopover ownFocus button={ diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx index 3d7f9568428ef..56420f98bfac7 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx @@ -38,6 +38,7 @@ export const ServiceProviderFilter: React.FC<Props> = ({ optionKeys, onChange }) options={options} renderOption={(option) => option.label} selectedOptionKeys={optionKeys} + dataTestSubj="service-field-endpoints" /> ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx index e9c32503dba73..071069a880b3c 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx @@ -38,6 +38,7 @@ export const TaskTypeFilter: React.FC<Props> = ({ optionKeys, onChange }) => { options={options} renderOption={(option) => option.label} selectedOptionKeys={optionKeys} + dataTestSubj="type-field-endpoints" /> ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.test.tsx new file mode 100644 index 0000000000000..bfdc1edd31bd6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { IndexItem } from './index_item'; +import { InferenceUsageInfo } from '../../../../types'; +import { useKibana } from '../../../../../../hooks/use_kibana'; + +jest.mock('../../../../../../hooks/use_kibana'); +const mockUseKibana = useKibana as jest.Mock; +const mockNavigateToApp = jest.fn(); + +describe('Index Item', () => { + const item: InferenceUsageInfo = { + id: 'index-1', + type: 'Index', + }; + beforeEach(() => { + mockUseKibana.mockReturnValue({ + services: { + application: { + navigateToApp: mockNavigateToApp, + }, + }, + }); + + render(<IndexItem usageItem={item} />); + }); + + it('renders', () => { + expect(screen.getByText('index-1')).toBeInTheDocument(); + expect(screen.getByText('Index')).toBeInTheDocument(); + }); + + it('opens index in a new tab', () => { + fireEvent.click(screen.getByRole('button')); + expect(mockNavigateToApp).toHaveBeenCalledWith('enterpriseSearchContent', { + openInNewTab: true, + path: 'search_indices/index-1', + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.tsx similarity index 77% rename from x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx rename to x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.tsx index 577b9f8aa0e29..a62a9b9f3caed 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/index_item.tsx @@ -16,34 +16,35 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import { ENTERPRISE_SEARCH_CONTENT_APP_ID } from '@kbn/deeplinks-search'; -import { MANAGEMENT_APP_ID } from '@kbn/deeplinks-management/constants'; + +import { SEARCH_INDICES } from '@kbn/deeplinks-search/constants'; import { useKibana } from '../../../../../../hooks/use_kibana'; import { InferenceUsageInfo } from '../../../../types'; -import { PIPELINE_URL } from '../../../../constants'; +import { SERVERLESS_INDEX_MANAGEMENT_URL } from '../../../../constants'; interface UsageProps { usageItem: InferenceUsageInfo; } -export const UsageItem: React.FC<UsageProps> = ({ usageItem }) => { +export const IndexItem: React.FC<UsageProps> = ({ usageItem }) => { const { - services: { application }, + services: { application, serverless }, } = useKibana(); - const handleNavigateToIndex = () => { - if (usageItem.type === 'Index') { - application?.navigateToApp(ENTERPRISE_SEARCH_CONTENT_APP_ID, { - path: `search_indices/${usageItem.id}`, + const navigateToIndex = useCallback(() => { + if (serverless) { + application?.navigateToApp(SEARCH_INDICES, { + path: `${SERVERLESS_INDEX_MANAGEMENT_URL}/${usageItem.id}/data`, openInNewTab: true, }); - } else if (usageItem.type === 'Pipeline') { - application?.navigateToApp(MANAGEMENT_APP_ID, { - path: `${PIPELINE_URL}?pipeline=${usageItem.id}`, + } else { + application?.navigateToApp(ENTERPRISE_SEARCH_CONTENT_APP_ID, { + path: `search_indices/${usageItem.id}`, openInNewTab: true, }); } - }; + }, [application, serverless, usageItem.id]); return ( <EuiFlexGroup gutterSize="s" direction="column" data-test-subj="usageItem"> @@ -62,7 +63,7 @@ export const UsageItem: React.FC<UsageProps> = ({ usageItem }) => { </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiLink data-test-subj="navigateToIndexPage" onClick={handleNavigateToIndex}> + <EuiLink data-test-subj="navigateToIndexPage" onClick={navigateToIndex}> <EuiIcon size="s" type="popout" /> </EuiLink> </EuiFlexItem> diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx index d42b0f6735252..05aaaa8bb9eaf 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx @@ -10,7 +10,8 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { InferenceUsageInfo } from '../../../../types'; import * as i18n from '../delete/confirm_delete_endpoint/translations'; -import { UsageItem } from './usage_item'; +import { IndexItem } from './index_item'; +import { PipelineItem } from './pipeline_item'; interface ListUsageResultsProps { list: InferenceUsageInfo[]; @@ -35,9 +36,13 @@ export const ListUsageResults: React.FC<ListUsageResultsProps> = ({ list }) => { <EuiFlexItem> {list .filter((item) => item.id.toLowerCase().includes(term.toLowerCase())) - .map((item, id) => ( - <UsageItem usageItem={item} key={id} /> - ))} + .map((item, id) => { + if (item.type === 'Pipeline') { + return <PipelineItem usageItem={item} key={id} />; + } else { + return <IndexItem usageItem={item} key={id} />; + } + })} </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.test.tsx similarity index 59% rename from x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx rename to x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.test.tsx index 6c6899c71922d..8a1bc7a78cab2 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.test.tsx @@ -8,7 +8,7 @@ import { render, fireEvent, screen } from '@testing-library/react'; import React from 'react'; -import { UsageItem } from './usage_item'; +import { PipelineItem } from './pipeline_item'; import { InferenceUsageInfo } from '../../../../types'; import { useKibana } from '../../../../../../hooks/use_kibana'; @@ -16,7 +16,11 @@ jest.mock('../../../../../../hooks/use_kibana'); const mockUseKibana = useKibana as jest.Mock; const mockNavigateToApp = jest.fn(); -describe('UsageItem', () => { +describe('Pipeline item', () => { + const item: InferenceUsageInfo = { + id: 'pipeline-1', + type: 'Pipeline', + }; beforeEach(() => { mockUseKibana.mockReturnValue({ services: { @@ -25,41 +29,10 @@ describe('UsageItem', () => { }, }, }); - }); - - describe('index', () => { - const item: InferenceUsageInfo = { - id: 'index-1', - type: 'Index', - }; - - beforeEach(() => { - render(<UsageItem usageItem={item} />); - }); - - it('renders', () => { - expect(screen.getByText('index-1')).toBeInTheDocument(); - expect(screen.getByText('Index')).toBeInTheDocument(); - }); - - it('opens index in a new tab', () => { - fireEvent.click(screen.getByRole('button')); - expect(mockNavigateToApp).toHaveBeenCalledWith('enterpriseSearchContent', { - openInNewTab: true, - path: 'search_indices/index-1', - }); - }); + render(<PipelineItem usageItem={item} />); }); describe('pipeline', () => { - const item: InferenceUsageInfo = { - id: 'pipeline-1', - type: 'Pipeline', - }; - - beforeEach(() => { - render(<UsageItem usageItem={item} />); - }); it('renders', () => { expect(screen.getByText('pipeline-1')).toBeInTheDocument(); expect(screen.getByText('Pipeline')).toBeInTheDocument(); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.tsx new file mode 100644 index 0000000000000..1a2e8ead29080 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/pipeline_item.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiText, + EuiTextTruncate, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { MANAGEMENT_APP_ID } from '@kbn/deeplinks-management/constants'; + +import { useKibana } from '../../../../../../hooks/use_kibana'; +import { InferenceUsageInfo } from '../../../../types'; +import { PIPELINE_URL } from '../../../../constants'; + +interface UsageProps { + usageItem: InferenceUsageInfo; +} +export const PipelineItem: React.FC<UsageProps> = ({ usageItem }) => { + const { + services: { application }, + } = useKibana(); + const navigateToPipeline = useCallback(() => { + application?.navigateToApp(MANAGEMENT_APP_ID, { + path: `${PIPELINE_URL}?pipeline=${usageItem.id}`, + openInNewTab: true, + }); + }, [application, usageItem.id]); + + return ( + <EuiFlexGroup gutterSize="s" direction="column" data-test-subj="usageItem"> + <EuiFlexItem grow={false}> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiText size="s"> + <EuiTextTruncate text={usageItem.id} /> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiBadge color="hollow">{usageItem.type}</EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink data-test-subj="navigateToPipelinePage" onClick={navigateToPipeline}> + <EuiIcon size="s" type="popout" /> + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiHorizontalRule margin="none" /> + <EuiSpacer size="s" /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx index 33d7a4dae891f..cab278f5e1ed9 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx @@ -19,6 +19,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/react'; import { ENTERPRISE_SEARCH_CONTENT_APP_ID } from '@kbn/deeplinks-search'; +import { SEARCH_INDICES } from '@kbn/deeplinks-search/constants'; import { InferenceUsageInfo } from '../../../../types'; import { useKibana } from '../../../../../../hooks/use_kibana'; import { RenderMessageWithIcon } from './render_message_with_icon'; @@ -37,13 +38,19 @@ export const ScanUsageResults: React.FC<ScanUsageResultsProps> = ({ onIgnoreWarningCheckboxChange, }) => { const { - services: { application }, + services: { application, serverless }, } = useKibana(); const handleNavigateToIndexManagement = () => { - application?.navigateToApp(ENTERPRISE_SEARCH_CONTENT_APP_ID, { - path: 'search_indices', - openInNewTab: true, - }); + if (serverless) { + application?.navigateToApp(SEARCH_INDICES, { + openInNewTab: true, + }); + } else { + application?.navigateToApp(ENTERPRISE_SEARCH_CONTENT_APP_ID, { + path: `search_indices`, + openInNewTab: true, + }); + } }; return ( diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx index 06bd585c0eb28..345f0f81b0927 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx @@ -86,6 +86,7 @@ export const ConfirmDeleteEndpointModal: React.FC<ConfirmDeleteEndpointModalProp font-family: ${euiThemeVars.euiCodeFontFamily}; font-weight: ${euiThemeVars.euiCodeFontWeightBold}; `} + data-test-subj="deleteModalInferenceEndpointName" > {inferenceEndpoint.endpoint} </EuiText> diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx index 4cf4b3112396d..0ea17fa6408a0 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx @@ -52,6 +52,7 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints }) { field: 'endpoint', name: i18n.ENDPOINT, + 'data-test-subj': 'endpointCell', render: (endpoint: string) => { if (endpoint) { return <EndpointInfo inferenceId={endpoint} />; @@ -65,6 +66,7 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints }) { field: 'provider', name: i18n.SERVICE_PROVIDER, + 'data-test-subj': 'providerCell', render: (provider: InferenceAPIConfigResponse) => { if (provider) { return <ServiceProvider providerEndpoint={provider} />; @@ -78,6 +80,7 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints }) { field: 'type', name: i18n.TASK_TYPE, + 'data-test-subj': 'typeCell', render: (type: TaskTypes) => { if (type) { return <TaskType type={type} />; @@ -149,6 +152,7 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints }) onChange={handleTableChange} pagination={pagination} sorting={sorting} + data-test-subj="inferenceEndpointTable" /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss deleted file mode 100644 index b85859948b0be..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss +++ /dev/null @@ -1,3 +0,0 @@ -.addEmptyPrompt { - max-width: 860px; -} \ No newline at end of file diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx deleted file mode 100644 index 755c1f0a1de54..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx +++ /dev/null @@ -1,47 +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 { screen } from '@testing-library/react'; -import { AddEmptyPrompt } from './add_empty_prompt'; - -import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers'; -import '@testing-library/jest-dom'; - -describe('When empty prompt is loaded', () => { - beforeEach(() => { - render(<AddEmptyPrompt />); - }); - - it('should display the description for creation of the first inference endpoint', () => { - expect( - screen.getByText( - /Inference endpoints enable you to perform inference tasks using NLP models provided by third-party services/ - ) - ).toBeInTheDocument(); - }); - - it('should have a learn-more link', () => { - const learnMoreLink = screen.getByTestId('learn-how-to-create-inference-endpoints'); - expect(learnMoreLink).toBeInTheDocument(); - }); - - it('should have a view-your-models link', () => { - const learnMoreLink = screen.getByTestId('view-your-models'); - expect(learnMoreLink).toBeInTheDocument(); - }); - - it('should have a semantic-search-with-elser link', () => { - const learnMoreLink = screen.getByTestId('semantic-search-with-elser'); - expect(learnMoreLink).toBeInTheDocument(); - }); - - it('should have a semantic-search-with-e5 link', () => { - const learnMoreLink = screen.getByTestId('semantic-search-with-e5'); - expect(learnMoreLink).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx deleted file mode 100644 index 782994975ada4..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx +++ /dev/null @@ -1,103 +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 { - EuiPageTemplate, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; -import { docLinks } from '../../../common/doc_links'; - -import * as i18n from '../../../common/translations'; - -import inferenceEndpoint from '../../assets/images/inference_endpoint.svg'; - -import { EndpointPrompt } from './endpoint_prompt'; -import { useTrainedModelPageUrl } from '../../hooks/use_trained_model_page_url'; - -import './add_empty_prompt.scss'; - -export const AddEmptyPrompt: React.FC = () => { - const trainedModelPageUrl = useTrainedModelPageUrl(); - - return ( - <EuiPageTemplate.EmptyPrompt - layout="horizontal" - restrictWidth - color="plain" - hasShadow - icon={<EuiImage size="fullWidth" src={inferenceEndpoint} alt="" />} - title={<h2>{i18n.INFERENCE_ENDPOINT_LABEL}</h2>} - body={ - <EuiFlexGroup direction="column"> - <EuiFlexItem data-test-subj="createFirstInferenceEndpointDescription"> - {i18n.CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION} - </EuiFlexItem> - <EuiFlexItem> - <EuiLink - href={docLinks.createInferenceEndpoint} - target="_blank" - data-test-subj="learn-how-to-create-inference-endpoints" - > - {i18n.LEARN_HOW_TO_CREATE_INFERENCE_ENDPOINTS_LINK} - </EuiLink> - </EuiFlexItem> - <EuiFlexItem> - <EuiLink href={trainedModelPageUrl} target="_blank" data-test-subj="view-your-models"> - {i18n.VIEW_YOUR_MODELS_LINK} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - } - footer={ - <EuiFlexGroup gutterSize="xs" direction="column"> - <EuiFlexItem> - <strong>{i18n.START_WITH_PREPARED_ENDPOINTS_LABEL}</strong> - </EuiFlexItem> - <EuiSpacer size="s" /> - <EuiFlexGroup> - <EuiFlexItem> - <EndpointPrompt - title={i18n.ELSER_TITLE} - description={i18n.ELSER_DESCRIPTION} - footer={ - <EuiLink - href={docLinks.semanticSearchElser} - target="_blank" - data-test-subj="semantic-search-with-elser" - > - {i18n.SEMANTIC_SEARCH_WITH_ELSER_LINK} - </EuiLink> - } - /> - </EuiFlexItem> - <EuiFlexItem> - <EndpointPrompt - title={i18n.E5_TITLE} - description={i18n.E5_DESCRIPTION} - footer={ - <EuiLink - href={docLinks.semanticSearchE5} - target="_blank" - data-test-subj="semantic-search-with-e5" - > - {i18n.SEMANTIC_SEARCH_WITH_E5_LINK} - </EuiLink> - } - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexGroup> - } - /> - ); -}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx deleted file mode 100644 index aa6ff9d582e10..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.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 React from 'react'; -import { EuiCard } from '@elastic/eui'; - -interface EndpointPromptProps { - title: string; - description: string; - footer: React.ReactElement; -} - -export const EndpointPrompt: React.FC<EndpointPromptProps> = ({ title, description, footer }) => ( - <EuiCard - display="plain" - textAlign="left" - data-test-subj="multilingualE5PromptForEmptyState" - title={title} - titleSize="xs" - description={description} - footer={footer} - /> -); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx index e1dae72d402f7..c39bc69fc300b 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx @@ -11,7 +11,6 @@ import { EuiPageTemplate } from '@elastic/eui'; import { useQueryInferenceEndpoints } from '../hooks/use_inference_endpoints'; import { TabularPage } from './all_inference_endpoints/tabular_page'; -import { AddEmptyPrompt } from './empty_prompt/add_empty_prompt'; import { InferenceEndpointsHeader } from './inference_endpoints_header'; export const InferenceEndpoints: React.FC = () => { @@ -21,13 +20,9 @@ export const InferenceEndpoints: React.FC = () => { return ( <> - {inferenceEndpoints.length > 0 && <InferenceEndpointsHeader />} - <EuiPageTemplate.Section className="eui-yScroll"> - {inferenceEndpoints.length === 0 ? ( - <AddEmptyPrompt /> - ) : ( - <TabularPage inferenceEndpoints={inferenceEndpoints} /> - )} + <InferenceEndpointsHeader /> + <EuiPageTemplate.Section className="eui-yScroll" data-test-subj="inferenceManagementPage"> + <TabularPage inferenceEndpoints={inferenceEndpoints} /> </EuiPageTemplate.Section> </> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/locators.ts b/x-pack/plugins/search_inference_endpoints/public/locators.ts new file mode 100644 index 0000000000000..5b32e5648cc7c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/locators.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 type { LocatorDefinition } from '@kbn/share-plugin/common'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; + +import { PLUGIN_ID } from '../common/constants'; +import { SEARCH_INFERENCE_ENDPOINTS_PATH } from './routes'; + +export function registerLocators(share: SharePluginSetup) { + share.url.locators.create<SerializableRecord>(new SearchInferenceEndpointLocatorDefinition()); +} + +class SearchInferenceEndpointLocatorDefinition implements LocatorDefinition<SerializableRecord> { + public readonly getLocation = async () => { + return { + app: PLUGIN_ID, + path: SEARCH_INFERENCE_ENDPOINTS_PATH, + state: {}, + }; + }; + + public readonly id = 'SEARCH_INFERENCE_ENDPOINTS'; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/plugin.ts b/x-pack/plugins/search_inference_endpoints/public/plugin.ts index a864b7c0fcdd6..cb60f611b3bb3 100644 --- a/x-pack/plugins/search_inference_endpoints/public/plugin.ts +++ b/x-pack/plugins/search_inference_endpoints/public/plugin.ts @@ -12,16 +12,20 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; import { PLUGIN_ID, PLUGIN_NAME } from '../common/constants'; import { docLinks } from '../common/doc_links'; import { InferenceEndpoints, getInferenceEndpointsProvider } from './embeddable'; import { + AppPluginSetupDependencies, AppPluginStartDependencies, SearchInferenceEndpointsConfigType, SearchInferenceEndpointsPluginSetup, SearchInferenceEndpointsPluginStart, } from './types'; import { INFERENCE_ENDPOINTS_UI_FLAG } from '.'; +import { registerLocators } from './locators'; +import { INFERENCE_ENDPOINTS_PATH } from './components/routes'; export class SearchInferenceEndpointsPlugin implements Plugin<SearchInferenceEndpointsPluginSetup, SearchInferenceEndpointsPluginStart> @@ -33,7 +37,8 @@ export class SearchInferenceEndpointsPlugin } public setup( - core: CoreSetup<AppPluginStartDependencies, SearchInferenceEndpointsPluginStart> + core: CoreSetup<AppPluginStartDependencies, SearchInferenceEndpointsPluginStart>, + plugins: AppPluginSetupDependencies ): SearchInferenceEndpointsPluginSetup { if ( !this.config.ui?.enabled && @@ -42,7 +47,16 @@ export class SearchInferenceEndpointsPlugin return {}; core.application.register({ id: PLUGIN_ID, - appRoute: '/app/search_inference_endpoints', + appRoute: '/app/elasticsearch/relevance', + deepLinks: [ + { + id: 'inferenceEndpoints', + path: `/${INFERENCE_ENDPOINTS_PATH}`, + title: i18n.translate('xpack.searchInferenceEndpoints.InferenceEndpointsLinkLabel', { + defaultMessage: 'Inference Endpoints', + }), + }, + ], title: PLUGIN_NAME, async mount({ element, history }: AppMountParameters) { const { renderApp } = await import('./application'); @@ -56,6 +70,8 @@ export class SearchInferenceEndpointsPlugin }, }); + registerLocators(plugins.share); + return {}; } diff --git a/x-pack/plugins/search_inference_endpoints/public/types.ts b/x-pack/plugins/search_inference_endpoints/public/types.ts index 4bd83521cf8d6..9f73d0d0033b0 100644 --- a/x-pack/plugins/search_inference_endpoints/public/types.ts +++ b/x-pack/plugins/search_inference_endpoints/public/types.ts @@ -5,12 +5,14 @@ * 2.0. */ -import type { ConsolePluginStart } from '@kbn/console-plugin/public'; +import type { ConsolePluginSetup, ConsolePluginStart } from '@kbn/console-plugin/public'; import { HttpStart } from '@kbn/core-http-browser'; import { AppMountParameters } from '@kbn/core/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; +import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import React from 'react'; + +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { App } from './components/app'; import type { InferenceEndpointsProvider } from './providers/inference_endpoints_provider'; @@ -27,12 +29,21 @@ export interface AppPluginStartDependencies { history: AppMountParameters['history']; share: SharePluginStart; console?: ConsolePluginStart; + serverless?: ServerlessPluginStart; +} + +export interface AppPluginSetupDependencies { + history: AppMountParameters['history']; + share: SharePluginSetup; + console?: ConsolePluginSetup; } export interface AppServicesContext { http: HttpStart; ml?: MlPluginStart; console?: ConsolePluginStart; + serverless?: ServerlessPluginStart; + share: SharePluginStart; } export interface InferenceUsageResponse { diff --git a/x-pack/plugins/search_inference_endpoints/tsconfig.json b/x-pack/plugins/search_inference_endpoints/tsconfig.json index d454be99b65f0..9a5b160779e7a 100644 --- a/x-pack/plugins/search_inference_endpoints/tsconfig.json +++ b/x-pack/plugins/search_inference_endpoints/tsconfig.json @@ -33,7 +33,9 @@ "@kbn/features-plugin", "@kbn/ui-theme", "@kbn/deeplinks-search", - "@kbn/deeplinks-management" + "@kbn/deeplinks-management", + "@kbn/serverless", + "@kbn/utility-types" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx index 246d7b2ebf246..59a606fddafd5 100644 --- a/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx @@ -49,6 +49,7 @@ describe('SelectIndicesFlyout', () => { mockedUseQueryIndices.mockReturnValue({ indices: ['index1', 'index2', 'index3'], isLoading: false, + isFetched: true, }); }); diff --git a/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx b/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx index d98a6fc9de8b7..fa3868cb392c8 100644 --- a/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx +++ b/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx @@ -35,7 +35,7 @@ interface SelectIndicesFlyout { export const SelectIndicesFlyout: React.FC<SelectIndicesFlyout> = ({ onClose }) => { const [query, setQuery] = useState<string>(''); - const { indices, isLoading: isIndicesLoading } = useQueryIndices(query); + const { indices, isLoading: isIndicesLoading } = useQueryIndices({ query }); const { indices: selectedIndices, setIndices: setSelectedIndices } = useSourceIndicesFields(); const [selectedTempIndices, setSelectedTempIndices] = useState<string[]>(selectedIndices); const handleSelectOptions = (options: EuiSelectableOption[]) => { diff --git a/x-pack/plugins/search_playground/public/hooks/use_indices_validation.ts b/x-pack/plugins/search_playground/public/hooks/use_indices_validation.ts new file mode 100644 index 0000000000000..45b3848bb85e5 --- /dev/null +++ b/x-pack/plugins/search_playground/public/hooks/use_indices_validation.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 { useEffect, useState } from 'react'; +import { useQueryIndices } from './use_query_indices'; + +export const useIndicesValidation = (unvalidatedIndices: string[]) => { + const [isValidated, setIsValidated] = useState<boolean>(false); + const [validIndices, setValidIndices] = useState<string[]>([]); + const { indices, isFetched: isIndicesLoaded } = useQueryIndices({ + query: unvalidatedIndices.join(','), + exact: true, + }); + + useEffect(() => { + if (isIndicesLoaded) { + setValidIndices(indices.filter((index) => unvalidatedIndices.includes(index))); + setIsValidated(true); + } + }, [unvalidatedIndices, indices, isIndicesLoaded]); + + return { isValidated, validIndices }; +}; diff --git a/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts b/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts index d011b471a68d6..cc5812306097c 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts @@ -11,26 +11,41 @@ import { useKibana } from './use_kibana'; import { APIRoutes } from '../types'; export const useQueryIndices = ( - query: string = '' -): { indices: IndexName[]; isLoading: boolean } => { + { + query, + exact, + }: { + query?: string; + exact?: boolean; + } = { query: '', exact: false } +): { indices: IndexName[]; isLoading: boolean; isFetched: boolean } => { const { services } = useKibana(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isFetched } = useQuery({ queryKey: ['indices', query], queryFn: async () => { - const response = await services.http.get<{ - indices: string[]; - }>(APIRoutes.GET_INDICES, { - query: { - search_query: query, - size: 10, - }, - }); + try { + const response = await services.http.get<{ + indices: string[]; + }>(APIRoutes.GET_INDICES, { + query: { + search_query: query, + exact, + size: 50, + }, + }); - return response.indices; + return response.indices; + } catch (err) { + if (err?.response?.status === 404) { + return []; + } + + throw err; + } }, initialData: [], }); - return { indices: data, isLoading }; + return { indices: data, isLoading, isFetched }; }; diff --git a/x-pack/plugins/search_playground/public/providers/form_provider.test.tsx b/x-pack/plugins/search_playground/public/providers/form_provider.test.tsx index 96c86ad184931..e0205f01f1e6a 100644 --- a/x-pack/plugins/search_playground/public/providers/form_provider.test.tsx +++ b/x-pack/plugins/search_playground/public/providers/form_provider.test.tsx @@ -19,6 +19,9 @@ jest.mock('../hooks/use_llms_models'); jest.mock('react-router-dom-v5-compat', () => ({ useSearchParams: jest.fn(() => [{ get: jest.fn() }]), })); +jest.mock('../hooks/use_indices_validation', () => ({ + useIndicesValidation: jest.fn((indices) => ({ isValidated: true, validIndices: indices })), +})); let formHookSpy: jest.SpyInstance; @@ -216,6 +219,7 @@ describe('FormProvider', () => { }); it('updates indices from search params', async () => { + expect.assertions(1); const mockSearchParams = new URLSearchParams(); mockSearchParams.get = jest.fn().mockReturnValue('new-index'); mockUseSearchParams.mockReturnValue([mockSearchParams]); @@ -237,10 +241,12 @@ describe('FormProvider', () => { </FormProvider> ); - const { getValues } = formHookSpy.mock.results[0].value; + await act(async () => { + const { getValues } = formHookSpy.mock.results[0].value; - await waitFor(() => { - expect(getValues(ChatFormFields.indices)).toEqual(['new-index']); + await waitFor(() => { + expect(getValues(ChatFormFields.indices)).toEqual(['new-index']); + }); }); }); }); diff --git a/x-pack/plugins/search_playground/public/providers/form_provider.tsx b/x-pack/plugins/search_playground/public/providers/form_provider.tsx index f352688fe89f1..e1b27e66a98df 100644 --- a/x-pack/plugins/search_playground/public/providers/form_provider.tsx +++ b/x-pack/plugins/search_playground/public/providers/form_provider.tsx @@ -7,6 +7,7 @@ import { FormProvider as ReactHookFormProvider, useForm } from 'react-hook-form'; import React, { useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useIndicesValidation } from '../hooks/use_indices_validation'; import { useLoadFieldsByIndices } from '../hooks/use_load_fields_by_indices'; import { ChatForm, ChatFormFields } from '../types'; import { useLLMsModels } from '../hooks/use_llms_models'; @@ -53,16 +54,27 @@ export const FormProvider: React.FC<React.PropsWithChildren<FormProviderProps>> }) => { const models = useLLMsModels(); const [searchParams] = useSearchParams(); - const index = useMemo(() => searchParams.get('default-index'), [searchParams]); + const defaultIndex = useMemo(() => { + const index = searchParams.get('default-index'); + + return index ? [index] : null; + }, [searchParams]); const sessionState = useMemo(() => getLocalSession(storage), [storage]); const form = useForm<ChatForm>({ defaultValues: { ...sessionState, - indices: index ? [index] : sessionState.indices, + indices: [], search_query: '', }, }); - useLoadFieldsByIndices({ watch: form.watch, setValue: form.setValue, getValues: form.getValues }); + const { isValidated: isValidatedIndices, validIndices } = useIndicesValidation( + defaultIndex || sessionState.indices || [] + ); + useLoadFieldsByIndices({ + watch: form.watch, + setValue: form.setValue, + getValues: form.getValues, + }); useEffect(() => { const subscription = form.watch((values) => @@ -80,5 +92,11 @@ export const FormProvider: React.FC<React.PropsWithChildren<FormProviderProps>> } }, [form, models]); + useEffect(() => { + if (isValidatedIndices) { + form.setValue(ChatFormFields.indices, validIndices); + } + }, [form, isValidatedIndices, validIndices]); + return <ReactHookFormProvider {...form}>{children}</ReactHookFormProvider>; }; diff --git a/x-pack/plugins/search_playground/server/lib/fetch_indices.ts b/x-pack/plugins/search_playground/server/lib/fetch_indices.ts index 51b72790028c2..c60e6b5082610 100644 --- a/x-pack/plugins/search_playground/server/lib/fetch_indices.ts +++ b/x-pack/plugins/search_playground/server/lib/fetch_indices.ts @@ -21,11 +21,12 @@ function isClosed(index: IndicesIndexState): boolean { export const fetchIndices = async ( client: ElasticsearchClient, - searchQuery: string | undefined + searchQuery: string | undefined, + { exact }: { exact?: boolean } = { exact: false } ): Promise<{ indexNames: string[]; }> => { - const indexPattern = searchQuery ? `*${searchQuery}*` : '*'; + const indexPattern = exact && searchQuery ? searchQuery : searchQuery ? `*${searchQuery}*` : '*'; const allIndexMatches = await client.indices.get({ expand_wildcards: ['open'], // for better performance only compute aliases and settings of indices but not mappings diff --git a/x-pack/plugins/search_playground/server/routes.ts b/x-pack/plugins/search_playground/server/routes.ts index 3cdebe11c02c2..115b55d34146a 100644 --- a/x-pack/plugins/search_playground/server/routes.ts +++ b/x-pack/plugins/search_playground/server/routes.ts @@ -198,17 +198,17 @@ export function defineRoutes({ query: schema.object({ search_query: schema.maybe(schema.string()), size: schema.number({ defaultValue: 10, min: 0 }), + exact: schema.maybe(schema.boolean({ defaultValue: false })), }), }, }, errorHandler(logger)(async (context, request, response) => { - const { search_query: searchQuery, size } = request.query; + const { search_query: searchQuery, exact, size } = request.query; const { client: { asCurrentUser }, } = (await context.core).elasticsearch; - const { indexNames } = await fetchIndices(asCurrentUser, searchQuery); - + const { indexNames } = await fetchIndices(asCurrentUser, searchQuery, { exact }); const indexNameSlice = indexNames.slice(0, size).filter(isNotNullish); return response.ok({ diff --git a/x-pack/plugins/search_solution/search_navigation/README.mdx b/x-pack/plugins/search_solution/search_navigation/README.mdx new file mode 100644 index 0000000000000..13ece425b680f --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/README.mdx @@ -0,0 +1,3 @@ +# Search Navigation + +The Search Navigation plugin is used to handle navigation for search solution plugins across both stack and serverless. diff --git a/x-pack/plugins/lens/public/embeddable/index.ts b/x-pack/plugins/search_solution/search_navigation/common/index.ts similarity index 72% rename from x-pack/plugins/lens/public/embeddable/index.ts rename to x-pack/plugins/search_solution/search_navigation/common/index.ts index 50ee0f582a2ff..8a5bd50a5bc37 100644 --- a/x-pack/plugins/lens/public/embeddable/index.ts +++ b/x-pack/plugins/search_solution/search_navigation/common/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export * from './embeddable'; - -export { type LensApi, isLensApi } from './interfaces/lens_api'; +export const PLUGIN_ID = 'searchNavigation'; +export const PLUGIN_NAME = 'searchNavigation'; diff --git a/x-pack/plugins/search_solution/search_navigation/jest.config.js b/x-pack/plugins/search_solution/search_navigation/jest.config.js new file mode 100644 index 0000000000000..e86a30c143245 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['<rootDir>/x-pack/plugins/search_solution/search_navigation'], + coverageDirectory: + '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/search_solution/search_navigation', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '<rootDir>/x-pack/plugins/search_solution/search_navigation/{public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/search_solution/search_navigation/kibana.jsonc b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc new file mode 100644 index 0000000000000..4b10b5f3a5a78 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/kibana.jsonc @@ -0,0 +1,21 @@ +{ + "type": "plugin", + "id": "@kbn/search-navigation", + "owner": "@elastic/search-kibana", + "group": "search", + "visibility": "private", + "plugin": { + "id": "searchNavigation", + "server": false, + "browser": true, + "configPath": [ + "xpack", + "searchNavigation" + ], + "requiredPlugins": [], + "optionalPlugins": [ + "serverless" + ], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts new file mode 100644 index 0000000000000..1b17296f54c73 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.test.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart, ScopedHistory } from '@kbn/core/public'; +import type { ChromeNavLink } from '@kbn/core-chrome-browser'; + +import { classicNavigationFactory } from './classic_navigation'; +import { ClassicNavItem } from './types'; + +describe('classicNavigationFactory', function () { + const mockedNavLinks: Array<Partial<ChromeNavLink>> = [ + { + id: 'enterpriseSearch', + url: '/app/enterprise_search/overview', + title: 'Overview', + }, + { + id: 'enterpriseSearchContent:searchIndices', + title: 'Indices', + url: '/app/enterprise_search/content/search_indices', + }, + { + id: 'enterpriseSearchContent:connectors', + title: 'Connectors', + url: '/app/enterprise_search/content/connectors', + }, + { + id: 'enterpriseSearchContent:webCrawlers', + title: 'Web crawlers', + url: '/app/enterprise_search/content/crawlers', + }, + ]; + const mockedCoreStart = { + chrome: { + navLinks: { + getAll: () => mockedNavLinks, + }, + }, + }; + const core = mockedCoreStart as unknown as CoreStart; + const mockHistory = { + location: { + pathname: '/', + }, + createHref: jest.fn(), + }; + const history = mockHistory as unknown as ScopedHistory; + + beforeEach(() => { + jest.clearAllMocks(); + mockHistory.location.pathname = '/'; + mockHistory.createHref.mockReturnValue('/'); + }); + + it('can render top-level items', () => { + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ]; + expect(classicNavigationFactory(items, core, history)).toEqual({ + icon: 'logoEnterpriseSearch', + items: [ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ], + name: 'Elasticsearch', + }); + }); + + it('will set isSelected', () => { + mockHistory.location.pathname = '/overview'; + mockHistory.createHref.mockReturnValue('/app/enterprise_search/overview'); + + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: true, + name: 'Overview', + onClick: expect.any(Function), + }, + ]); + }); + it('can render items with children', () => { + const items: ClassicNavItem[] = [ + { + id: 'searchContent', + name: 'Content', + items: [ + { + id: 'searchIndices', + deepLink: { + link: 'enterpriseSearchContent:searchIndices', + }, + }, + { + id: 'searchConnectors', + deepLink: { + link: 'enterpriseSearchContent:connectors', + }, + }, + ], + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + id: 'searchContent', + items: [ + { + href: '/app/enterprise_search/content/search_indices', + id: 'searchIndices', + isSelected: false, + name: 'Indices', + onClick: expect.any(Function), + }, + { + href: '/app/enterprise_search/content/connectors', + id: 'searchConnectors', + isSelected: false, + name: 'Connectors', + onClick: expect.any(Function), + }, + ], + name: 'Content', + }, + ]); + }); + it('returns name if provided over the deeplink title', () => { + const items: ClassicNavItem[] = [ + { + id: 'searchIndices', + deepLink: { + link: 'enterpriseSearchContent:searchIndices', + }, + name: 'Index Management', + }, + ]; + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/content/search_indices', + id: 'searchIndices', + isSelected: false, + name: 'Index Management', + onClick: expect.any(Function), + }, + ]); + }); + it('removes item if deeplink not defined', () => { + const items: ClassicNavItem[] = [ + { + id: 'unit-test', + deepLink: { + link: 'enterpriseSearch', + }, + }, + { + id: 'serverlessElasticsearch', + deepLink: { + link: 'serverlessElasticsearch', + }, + }, + ]; + + const solutionNav = classicNavigationFactory(items, core, history); + expect(solutionNav!.items).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'unit-test', + isSelected: false, + name: 'Overview', + onClick: expect.any(Function), + }, + ]); + }); +}); diff --git a/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts new file mode 100644 index 0000000000000..99a6b33c4bcce --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/classic_navigation.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type MouseEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { CoreStart, ScopedHistory } from '@kbn/core/public'; +import type { ChromeNavLink, EuiSideNavItemTypeEnhanced } from '@kbn/core-chrome-browser'; +import type { SolutionNavProps } from '@kbn/shared-ux-page-solution-nav'; + +import type { ClassicNavItem, ClassicNavItemDeepLink, ClassicNavigationFactoryFn } from './types'; +import { stripTrailingSlash } from './utils'; + +type DeepLinksMap = Record<string, ChromeNavLink | undefined>; +type SolutionNavItems = SolutionNavProps['items']; + +export const classicNavigationFactory: ClassicNavigationFactoryFn = ( + classicItems: ClassicNavItem[], + core: CoreStart, + history: ScopedHistory<unknown> +): SolutionNavProps | undefined => { + const navLinks = core.chrome.navLinks.getAll(); + const deepLinks = navLinks.reduce((links: DeepLinksMap, link: ChromeNavLink) => { + links[link.id] = link; + return links; + }, {}); + + const currentPath = stripTrailingSlash(history.location.pathname); + const currentLocation = history.createHref({ pathname: currentPath }); + const items: SolutionNavItems = generateSideNavItems( + classicItems, + core, + deepLinks, + currentLocation + ); + + return { + items, + icon: 'logoEnterpriseSearch', + name: i18n.translate('xpack.searchNavigation.classicNav.name', { + defaultMessage: 'Elasticsearch', + }), + }; +}; + +function generateSideNavItems( + classicItems: ClassicNavItem[], + core: CoreStart, + deepLinks: DeepLinksMap, + currentLocation: string +): SolutionNavItems { + const result: SolutionNavItems = []; + + for (const navItem of classicItems) { + let children: SolutionNavItems | undefined; + + const { deepLink, items, ...rest } = navItem; + if (items) { + children = generateSideNavItems(items, core, deepLinks, currentLocation); + } + + let item: EuiSideNavItemTypeEnhanced<{}> | undefined; + if (deepLink) { + const sideNavProps = getSideNavItemLinkProps(deepLink, deepLinks, core, currentLocation); + if (sideNavProps) { + const { name, ...linkProps } = sideNavProps; + item = { + ...rest, + ...linkProps, + name: navItem?.name ?? name, + }; + } + } else { + item = { + ...rest, + items: children, + name: navItem.name, + }; + } + + if (isValidSideNavItem(item)) { + result.push(item); + } + } + + return result; +} + +function isValidSideNavItem( + item: EuiSideNavItemTypeEnhanced<unknown> | undefined +): item is EuiSideNavItemTypeEnhanced<unknown> { + if (item === undefined) return false; + if (item.href || item.onClick) return true; + if (item?.items?.length ?? 0 > 0) return true; + + return false; +} + +function getSideNavItemLinkProps( + { link, shouldShowActiveForSubroutes }: ClassicNavItemDeepLink, + deepLinks: DeepLinksMap, + core: CoreStart, + currentLocation: string +) { + const deepLink = deepLinks[link]; + if (!deepLink || !deepLink.url) return undefined; + const isSelected = Boolean( + deepLink.url === currentLocation || + (shouldShowActiveForSubroutes && currentLocation.startsWith(deepLink.url)) + ); + + return { + onClick: (e: MouseEvent) => { + e.preventDefault(); + core.application.navigateToUrl(deepLink.url); + }, + href: deepLink.url, + name: deepLink.title, + isSelected, + }; +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/index.ts b/x-pack/plugins/search_solution/search_navigation/public/index.ts new file mode 100644 index 0000000000000..f5fc03e088a2c --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/index.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 type { PluginInitializerContext } from '@kbn/core-plugins-browser'; +import { SearchNavigationPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SearchNavigationPlugin(initializerContext); +} + +export type { + SearchNavigationPluginSetup, + SearchNavigationPluginStart, + ClassicNavItem, + ClassicNavItemDeepLink, +} from './types'; diff --git a/x-pack/plugins/search_solution/search_navigation/public/plugin.ts b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts new file mode 100644 index 0000000000000..4b618a6245c40 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/plugin.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + ScopedHistory, +} from '@kbn/core/public'; +import type { ChromeStyle } from '@kbn/core-chrome-browser'; +import type { Logger } from '@kbn/logging'; +import type { + SearchNavigationPluginSetup, + SearchNavigationPluginStart, + ClassicNavItem, + ClassicNavigationFactoryFn, +} from './types'; + +export class SearchNavigationPlugin + implements Plugin<SearchNavigationPluginSetup, SearchNavigationPluginStart> +{ + private readonly logger: Logger; + private currentChromeStyle: ChromeStyle | undefined = undefined; + private baseClassicNavItemsFn: (() => ClassicNavItem[]) | undefined = undefined; + private coreStart: CoreStart | undefined = undefined; + private classicNavFactory: ClassicNavigationFactoryFn | undefined = undefined; + private onAppMountHandlers: Array<() => Promise<void>> = []; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public setup(_core: CoreSetup): SearchNavigationPluginSetup { + return {}; + } + + public start(core: CoreStart): SearchNavigationPluginStart { + this.coreStart = core; + core.chrome.getChromeStyle$().subscribe((value) => { + this.currentChromeStyle = value; + }); + + import('./classic_navigation').then(({ classicNavigationFactory }) => { + this.classicNavFactory = classicNavigationFactory; + }); + + return { + handleOnAppMount: this.handleOnAppMount.bind(this), + registerOnAppMountHandler: this.registerOnAppMountHandler.bind(this), + setGetBaseClassicNavItems: this.setGetBaseClassicNavItems.bind(this), + useClassicNavigation: this.useClassicNavigation.bind(this), + }; + } + + public stop() {} + + private async handleOnAppMount() { + if (this.onAppMountHandlers.length === 0) return; + + try { + await Promise.all(this.onAppMountHandlers); + } catch (e) { + this.logger.warn('Error handling app mount functions for search navigation'); + this.logger.warn(e); + } + } + + private registerOnAppMountHandler(handler: () => Promise<void>) { + this.onAppMountHandlers.push(handler); + } + + private setGetBaseClassicNavItems(classicNavItemsFn: () => ClassicNavItem[]) { + this.baseClassicNavItemsFn = classicNavItemsFn; + } + + private useClassicNavigation(history: ScopedHistory<unknown>) { + if ( + this.baseClassicNavItemsFn === undefined || + this.classicNavFactory === undefined || + this.coreStart === undefined || + this.currentChromeStyle !== 'classic' + ) + return undefined; + + return this.classicNavFactory(this.baseClassicNavItemsFn(), this.coreStart, history); + } +} diff --git a/x-pack/plugins/search_solution/search_navigation/public/types.ts b/x-pack/plugins/search_solution/search_navigation/public/types.ts new file mode 100644 index 0000000000000..91e8cc73524e2 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/types.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 type { ReactNode } from 'react'; +import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; +import type { CoreStart, ScopedHistory } from '@kbn/core/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SolutionNavProps } from '@kbn/shared-ux-page-solution-nav'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchNavigationPluginSetup {} + +export interface SearchNavigationPluginStart { + registerOnAppMountHandler: (onAppMount: () => Promise<void>) => void; + handleOnAppMount: () => Promise<void>; + // This is temporary until we can migrate building the class nav item list out of `enterprise_search` plugin + setGetBaseClassicNavItems: (classicNavItemsFn: () => ClassicNavItem[]) => void; + useClassicNavigation: (history: ScopedHistory<unknown>) => SolutionNavProps | undefined; +} + +export interface AppPluginSetupDependencies { + serverless?: ServerlessPluginSetup; +} + +export interface AppPluginStartDependencies { + serverless?: ServerlessPluginStart; +} + +export interface ClassicNavItemDeepLink { + link: AppDeepLinkId; + shouldShowActiveForSubroutes?: boolean; +} + +export interface ClassicNavItem { + 'data-test-subj'?: string; + deepLink?: ClassicNavItemDeepLink; + iconToString?: string; + id: string; + items?: ClassicNavItem[]; + name?: ReactNode; +} + +export type ClassicNavigationFactoryFn = ( + items: ClassicNavItem[], + core: CoreStart, + history: ScopedHistory<unknown> +) => SolutionNavProps | undefined; diff --git a/x-pack/plugins/search_solution/search_navigation/public/utils.ts b/x-pack/plugins/search_solution/search_navigation/public/utils.ts new file mode 100644 index 0000000000000..fb80778977b16 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/public/utils.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. + */ + +/** + * Helpers for stripping trailing or leading slashes from URLs or paths + * (usually ones that come in from React Router or API endpoints) + */ + +export const stripTrailingSlash = (url: string): string => { + return url && url.endsWith('/') ? url.slice(0, -1) : url; +}; + +export const stripLeadingSlash = (path: string): string => { + return path && path.startsWith('/') ? path.substring(1) : path; +}; diff --git a/x-pack/plugins/search_solution/search_navigation/tsconfig.json b/x-pack/plugins/search_solution/search_navigation/tsconfig.json new file mode 100644 index 0000000000000..6d61fbb24ec89 --- /dev/null +++ b/x-pack/plugins/search_solution/search_navigation/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "__mocks__/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/i18n", + "@kbn/core-chrome-browser", + "@kbn/shared-ux-page-solution-nav", + "@kbn/logging", + "@kbn/serverless", + "@kbn/core-plugins-browser", + ], + "exclude": ["target/**/*"] +} diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index b366a0e555357..265af5a47e1fe 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -138,6 +138,8 @@ export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const; export const APP_RESPONSE_ACTIONS_HISTORY_PATH = `${APP_PATH}${RESPONSE_ACTIONS_HISTORY_PATH}` as const; export const NOTES_PATH = `${MANAGEMENT_PATH}/notes` as const; +export const SIEM_MIGRATIONS_PATH = '/siem_migrations' as const; +export const SIEM_MIGRATIONS_RULES_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; // cloud logs to exclude from default index pattern export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*']; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 7fcdabad3b36c..3697359365619 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,7 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ // FIXME:PT delete? excludePoliciesInFilterEnabled: false, - kubernetesEnabled: true, + kubernetesEnabled: false, donutChartEmbeddablesEnabled: false, // Depends on https://github.com/elastic/kibana/issues/136409 item 2 - 6 /** diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index ac15080f2e0a4..36728e0e928a0 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -22,9 +22,8 @@ import { ElasticRulePartial, RuleMigrationTranslationResult, RuleMigrationComments, - RuleMigrationAllTaskStats, - RuleMigration, RuleMigrationTaskStats, + RuleMigration, RuleMigrationResourceData, RuleMigrationResourceType, RuleMigrationResource, @@ -44,7 +43,7 @@ export const CreateRuleMigrationResponse = z.object({ }); export type GetAllStatsRuleMigrationResponse = z.infer<typeof GetAllStatsRuleMigrationResponse>; -export const GetAllStatsRuleMigrationResponse = RuleMigrationAllTaskStats; +export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats); export type GetRuleMigrationRequestParams = z.infer<typeof GetRuleMigrationRequestParams>; export const GetRuleMigrationRequestParams = z.object({ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 7785304671129..fdb589e7b45cd 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -93,7 +93,9 @@ paths: content: application/json: schema: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationAllTaskStats' + type: array + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats' ## Specific rule migration APIs diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 0554ef18a13f7..2260b83190e22 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -88,6 +88,10 @@ export const ElasticRule = z.object({ * The Elastic prebuilt rule id matched. */ prebuilt_rule_id: NonEmptyString.optional(), + /** + * The Elastic integration IDs related to the rule. + */ + integration_ids: z.array(z.string()).optional(), /** * The Elastic rule id installed as a result. */ @@ -187,6 +191,10 @@ export const RuleMigration = z */ export type RuleMigrationTaskStats = z.infer<typeof RuleMigrationTaskStats>; export const RuleMigrationTaskStats = z.object({ + /** + * The migration id + */ + id: NonEmptyString, /** * Indicates if the migration task status. */ @@ -216,24 +224,16 @@ export const RuleMigrationTaskStats = z.object({ */ failed: z.number().int(), }), + /** + * The moment the migration was created. + */ + created_at: z.string(), /** * The moment of the last update. */ - last_updated_at: z.string().optional(), + last_updated_at: z.string(), }); -export type RuleMigrationAllTaskStats = z.infer<typeof RuleMigrationAllTaskStats>; -export const RuleMigrationAllTaskStats = z.array( - RuleMigrationTaskStats.merge( - z.object({ - /** - * The migration id - */ - migration_id: NonEmptyString, - }) - ) -); - /** * The type of the rule migration resource. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 95ff05df39a15..17c70665b9ad3 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -73,6 +73,11 @@ components: prebuilt_rule_id: description: The Elastic prebuilt rule id matched. $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + integration_ids: + type: array + items: + type: string + description: The Elastic integration IDs related to the rule. id: description: The Elastic rule id installed as a result. $ref: './common.schema.yaml#/components/schemas/NonEmptyString' @@ -140,9 +145,15 @@ components: type: object description: The rule migration task stats object. required: + - id - status - rules + - created_at + - last_updated_at properties: + id: + description: The migration id + $ref: './common.schema.yaml#/components/schemas/NonEmptyString' status: type: string description: Indicates if the migration task status. @@ -176,23 +187,13 @@ components: failed: type: integer description: The number of rules that have failed migration. + created_at: + type: string + description: The moment the migration was created. last_updated_at: type: string description: The moment of the last update. - RuleMigrationAllTaskStats: - type: array - items: - allOf: - - $ref: '#/components/schemas/RuleMigrationTaskStats' - - type: object - required: - - migration_id - properties: - migration_id: - description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' - RuleMigrationTranslationResult: type: string description: The rule translation result. diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts deleted file mode 100644 index 33d0c226fc44e..0000000000000 --- a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts +++ /dev/null @@ -1,94 +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 { EventHit } from '../search_strategy'; -import { getDataFromFieldsHits, getDataSafety } from './field_formatters'; -import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid'; - -describe('Events Details Helpers', () => { - const fields: EventHit['fields'] = eventHit.fields; - const resultFields = eventDetailsFormattedFields; - describe('#getDataFromFieldsHits', () => { - it('happy path', () => { - const result = getDataFromFieldsHits(fields); - expect(result).toEqual(resultFields); - }); - it('lets get weird', () => { - const whackFields = { - 'crazy.pants': [ - { - 'matched.field': ['matched_field'], - first_seen: ['2021-02-22T17:29:25.195Z'], - provider: ['yourself'], - type: ['custom'], - 'matched.atomic': ['matched_atomic'], - lazer: [ - { - 'great.field': ['grrrrr'], - lazer: [ - { - lazer: [ - { - cool: true, - lazer: [ - { - lazer: [ - { - lazer: [ - { - lazer: [ - { - whoa: false, - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - lazer: [ - { - cool: false, - }, - ], - }, - ], - }, - { - 'great.field': ['grrrrr_2'], - }, - ], - }, - ], - }; - const whackResultFields = [ - { - category: 'crazy', - field: 'crazy.pants', - values: [ - '{"matched.field":["matched_field"],"first_seen":["2021-02-22T17:29:25.195Z"],"provider":["yourself"],"type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"],"lazer":[{"lazer":[{"cool":true,"lazer":[{"lazer":[{"lazer":[{"lazer":[{"whoa":false}]}]}]}]}]},{"lazer":[{"cool":false}]}]},{"great.field":["grrrrr_2"]}]}', - ], - originalValue: [ - '{"matched.field":["matched_field"],"first_seen":["2021-02-22T17:29:25.195Z"],"provider":["yourself"],"type":["custom"],"matched.atomic":["matched_atomic"],"lazer":[{"great.field":["grrrrr"],"lazer":[{"lazer":[{"cool":true,"lazer":[{"lazer":[{"lazer":[{"lazer":[{"whoa":false}]}]}]}]}]},{"lazer":[{"cool":false}]}]},{"great.field":["grrrrr_2"]}]}', - ], - isObjectArray: true, - }, - ]; - const result = getDataFromFieldsHits(whackFields); - expect(result).toEqual(whackResultFields); - }); - }); - it('#getDataSafety', async () => { - const result = await getDataSafety(getDataFromFieldsHits, fields); - expect(result).toEqual(resultFields); - }); -}); diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.ts deleted file mode 100644 index 1d8c05ec9ccf7..0000000000000 --- a/x-pack/plugins/security_solution/common/utils/field_formatters.ts +++ /dev/null @@ -1,148 +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 { ecsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; -import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; -import { technicalRuleFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/technical_rule_field_map'; -import { isEmpty } from 'lodash/fp'; -import { ENRICHMENT_DESTINATION_PATH } from '../constants'; - -import type { Fields, TimelineEventsDetailsItem } from '../search_strategy'; -import { toObjectArrayOfStrings, toStringArray } from './to_array'; - -export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; - -export const getFieldCategory = (field: string): string => { - const fieldCategory = field.split('.')[0]; - if (!isEmpty(fieldCategory) && baseCategoryFields.includes(fieldCategory)) { - return 'base'; - } - return fieldCategory; -}; - -export const formatGeoLocation = (item: unknown[]) => { - const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; - if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { - try { - return toStringArray({ - lon: itemGeo.coordinates[0], - lat: itemGeo.coordinates[1], - }); - } catch { - return toStringArray(item); - } - } - return toStringArray(item); -}; - -export const isGeoField = (field: string) => - field.includes('geo.location') || field.includes('geoip.location'); - -export const isThreatEnrichmentFieldOrSubfield = (field: string, prependField?: string) => - prependField?.includes(ENRICHMENT_DESTINATION_PATH) || field === ENRICHMENT_DESTINATION_PATH; - -export const getDataFromFieldsHits = ( - fields: Fields, - prependField?: string, - prependFieldCategory?: string -): TimelineEventsDetailsItem[] => - Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((accumulator, field) => { - const item: unknown[] = fields[field]; - - const fieldCategory = - prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); - if (isGeoField(field)) { - return [ - ...accumulator, - { - category: fieldCategory, - field, - values: formatGeoLocation(item), - originalValue: formatGeoLocation(item), - isObjectArray: true, // important for UI - }, - ]; - } - - const objArrStr = toObjectArrayOfStrings(item); - const strArr = objArrStr.map(({ str }) => str); - const isObjectArray = objArrStr.some((o) => o.isObjectArray); - const dotField = prependField ? `${prependField}.${field}` : field; - - // return simple field value (non-esc object, non-array) - if ( - !isObjectArray || - Object.keys({ ...ecsFieldMap, ...technicalRuleFieldMap, ...legacyExperimentalFieldMap }).find( - (ecsField) => ecsField === field - ) === undefined - ) { - return [ - ...accumulator, - { - category: fieldCategory, - field: dotField, - values: strArr, - originalValue: strArr, - isObjectArray, - }, - ]; - } - - const threatEnrichmentObject = isThreatEnrichmentFieldOrSubfield(field, prependField) - ? [ - { - category: fieldCategory, - field: dotField, - values: strArr, - originalValue: strArr, - isObjectArray, - }, - ] - : []; - - // format nested fields - const nestedFields = Array.isArray(item) - ? item - .reduce<TimelineEventsDetailsItem[][]>((acc, curr) => { - acc.push(getDataFromFieldsHits(curr as Fields, dotField, fieldCategory)); - return acc; - }, []) - .flat() - : getDataFromFieldsHits(item, prependField, fieldCategory); - - // combine duplicate fields - const flat: Record<string, TimelineEventsDetailsItem> = [ - ...accumulator, - ...nestedFields, - ...threatEnrichmentObject, - ].reduce( - (acc, f) => ({ - ...acc, - // acc/flat is hashmap to determine if we already have the field or not without an array iteration - // its converted back to array in return with Object.values - ...(acc[f.field] != null - ? { - [f.field]: { - ...f, - originalValue: acc[f.field].originalValue.includes(f.originalValue[0]) - ? acc[f.field].originalValue - : [...acc[f.field].originalValue, ...f.originalValue], - values: acc[f.field].values?.includes(f.values?.[0] || '') - ? acc[f.field].values - : [...(acc[f.field].values || []), ...(f.values || [])], - }, - } - : { [f.field]: f }), - }), - {} as Record<string, TimelineEventsDetailsItem> - ); - - return Object.values(flat); - }, []); - -export const getDataSafety = <A, T>(fn: (args: A) => T, args: A): Promise<T> => - new Promise((resolve) => setTimeout(() => resolve(fn(args)))); diff --git a/x-pack/plugins/security_solution/common/utils/to_array.ts b/x-pack/plugins/security_solution/common/utils/to_array.ts deleted file mode 100644 index b6945708ff0db..0000000000000 --- a/x-pack/plugins/security_solution/common/utils/to_array.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. - */ - -export const toArray = <T = string>(value: T | T[] | null | undefined): T[] => - Array.isArray(value) ? value : value == null ? [] : [value]; - -export const toStringArray = <T = string>(value: T | T[] | null): string[] => { - if (Array.isArray(value)) { - return value.reduce<string[]>((acc, v) => { - if (v != null) { - switch (typeof v) { - case 'number': - case 'boolean': - return [...acc, v.toString()]; - case 'object': - try { - return [...acc, JSON.stringify(v)]; - } catch { - return [...acc, 'Invalid Object']; - } - case 'string': - return [...acc, v]; - default: - return [...acc, `${v}`]; - } - } - return acc; - }, []); - } else if (value == null) { - return []; - } else if (!Array.isArray(value) && typeof value === 'object') { - try { - return [JSON.stringify(value)]; - } catch { - return ['Invalid Object']; - } - } else { - return [`${value}`]; - } -}; - -export const toObjectArrayOfStrings = <T = string>( - value: T | T[] | null -): Array<{ - str: string; - isObjectArray?: boolean; -}> => { - if (Array.isArray(value)) { - return value.reduce< - Array<{ - str: string; - isObjectArray?: boolean; - }> - >((acc, v) => { - if (v != null) { - switch (typeof v) { - case 'number': - case 'boolean': - return [...acc, { str: v.toString() }]; - case 'object': - try { - return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value - } catch { - return [...acc, { str: 'Invalid Object' }]; - } - case 'string': - return [...acc, { str: v }]; - default: - return [...acc, { str: `${v}` }]; - } - } - return acc; - }, []); - } else if (value == null) { - return []; - } else if (!Array.isArray(value) && typeof value === 'object') { - try { - return [{ str: JSON.stringify(value), isObjectArray: true }]; - } catch { - return [{ str: 'Invalid Object' }]; - } - } else { - return [{ str: `${value}` }]; - } -}; diff --git a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts index b1a272de4d37e..ef920458f51dd 100644 --- a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts @@ -4,10 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import type { CellValueContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; -import { LENS_EMBEDDABLE_TYPE } from '@kbn/lens-plugin/public'; import type { SecurityAppStore } from '../../../../common/store/types'; import { createAddToTimelineLensAction, getInvestigatedValue } from './add_to_timeline'; import { KibanaServices } from '../../../../common/lib/kibana'; @@ -16,6 +15,9 @@ import type { DataProvider } from '../../../../../common/types'; import { TimelineId, EXISTS_OPERATOR } from '../../../../../common/types'; import { addProvider } from '../../../../timelines/store/actions'; import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { Query, Filter, AggregateQuery, TimeRange } from '@kbn/es-query'; +import type { LensApi } from '@kbn/lens-plugin/public'; +import { getLensApiMock } from '@kbn/lens-plugin/public/react_embeddable/mocks'; jest.mock('../../../../common/lib/kibana'); const currentAppId$ = new Subject<string | undefined>(); @@ -29,16 +31,32 @@ const store = { dispatch: mockDispatch, } as unknown as SecurityAppStore; -class MockEmbeddable { - public type; - constructor(type: string) { - this.type = type; - } - getFilters() {} - getQuery() {} -} +const getMockLensApi = ( + { from, to = 'now' }: { from: string; to: string } = { from: 'now-24h', to: 'now' } +): LensApi => + getLensApiMock({ + timeRange$: new BehaviorSubject<TimeRange | undefined>({ from, to }), + getViewUnderlyingDataArgs: jest.fn(() => ({ + dataViewSpec: { id: 'index-pattern-id' }, + timeRange: { from: 'now-7d', to: 'now' }, + filters: [], + query: undefined, + columns: [], + })), + saveToLibrary: jest.fn(async () => 'saved-id'), + }); + +const getMockEmbeddable = (type: string): IEmbeddable => + ({ + type, + filters$: new BehaviorSubject<Filter[] | undefined>([]), + query$: new BehaviorSubject<Query | AggregateQuery | undefined>({ + query: 'test', + language: 'kuery', + }), + } as unknown as IEmbeddable); -const lensEmbeddable = new MockEmbeddable(LENS_EMBEDDABLE_TYPE) as unknown as IEmbeddable; +const lensEmbeddable = getMockLensApi(); const columnMeta = { field: 'user.name', @@ -85,7 +103,7 @@ describe('createAddToTimelineLensAction', () => { expect( await addToTimelineAction.isCompatible({ ...context, - embeddable: new MockEmbeddable('not_lens') as unknown as IEmbeddable, + embeddable: getMockEmbeddable('not_lens') as unknown as IEmbeddable, }) ).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts index 84c95fd659fba..3ccbd30efd614 100644 --- a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts +++ b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts @@ -6,14 +6,16 @@ */ import type { CellValueContext, IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { isErrorEmbeddable, isFilterableEmbeddable } from '@kbn/embeddable-plugin/public'; +import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { createAction } from '@kbn/ui-actions-plugin/public'; +import { apiPublishesUnifiedSearch } from '@kbn/presentation-publishing'; +import { isLensApi } from '@kbn/lens-plugin/public'; import { KibanaServices } from '../../../../common/lib/kibana'; import type { SecurityAppStore } from '../../../../common/store/types'; import { addProvider } from '../../../../timelines/store/actions'; import type { DataProvider } from '../../../../../common/types'; import { EXISTS_OPERATOR, TimelineId } from '../../../../../common/types'; -import { fieldHasCellActions, isInSecurityApp, isLensEmbeddable } from '../../utils'; +import { fieldHasCellActions, isInSecurityApp } from '../../utils'; import { ADD_TO_TIMELINE, ADD_TO_TIMELINE_FAILED_TEXT, @@ -83,8 +85,8 @@ export const createAddToTimelineLensAction = ({ getDisplayName: () => ADD_TO_TIMELINE, isCompatible: async ({ embeddable, data }) => !isErrorEmbeddable(embeddable as IEmbeddable) && - isLensEmbeddable(embeddable as IEmbeddable) && - isFilterableEmbeddable(embeddable as IEmbeddable) && + isLensApi(embeddable) && + apiPublishesUnifiedSearch(embeddable) && isDataColumnsFilterable(data) && isInSecurityApp(currentAppId), execute: async ({ data }) => { diff --git a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.test.ts b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.test.ts index bb036e3f12e07..0ec4e00848348 100644 --- a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.test.ts +++ b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.test.ts @@ -7,12 +7,14 @@ import type { CellValueContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; -import { LENS_EMBEDDABLE_TYPE } from '@kbn/lens-plugin/public'; +import type { LensApi } from '@kbn/lens-plugin/public'; import { createCopyToClipboardLensAction } from './copy_to_clipboard'; import { KibanaServices } from '../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../common/constants'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import type { TimeRange } from '@kbn/es-query'; +import { getLensApiMock } from '@kbn/lens-plugin/public/react_embeddable/mocks'; jest.mock('../../../../common/lib/kibana'); const currentAppId$ = new Subject<string | undefined>(); @@ -23,14 +25,29 @@ KibanaServices.get().notifications.toasts.addSuccess = mockSuccessToast; const mockCopy = jest.fn((text: string) => true); jest.mock('copy-to-clipboard', () => (text: string) => mockCopy(text)); -class MockEmbeddable { - public type; - constructor(type: string) { - this.type = type; - } - getFilters() {} - getQuery() {} -} +const getMockLensApi = ( + { from, to = 'now' }: { from: string; to: string } = { from: 'now-24h', to: 'now' } +): LensApi => + getLensApiMock({ + timeRange$: new BehaviorSubject<TimeRange | undefined>({ from, to }), + getViewUnderlyingDataArgs: jest.fn(() => ({ + dataViewSpec: { id: 'index-pattern-id' }, + timeRange: { from: 'now-7d', to: 'now' }, + filters: [], + query: undefined, + columns: [], + })), + saveToLibrary: jest.fn(async () => 'saved-id'), + }); + +const getMockEmbeddable = (type: string): IEmbeddable => + ({ + type, + getFilters: jest.fn(), + getQuery: jest.fn(), + } as unknown as IEmbeddable); + +const lensEmbeddable = getMockLensApi(); const columnMeta = { field: 'user.name', @@ -39,7 +56,6 @@ const columnMeta = { sourceParams: { indexPatternId: 'some-pattern-id' }, }; const data: CellValueContext['data'] = [{ columnMeta, value: 'the value' }]; -const lensEmbeddable = new MockEmbeddable(LENS_EMBEDDABLE_TYPE) as unknown as IEmbeddable; const context = { data, @@ -76,7 +92,7 @@ describe('createCopyToClipboardLensAction', () => { expect( await copyToClipboardAction.isCompatible({ ...context, - embeddable: new MockEmbeddable('not_lens') as unknown as IEmbeddable, + embeddable: getMockEmbeddable('not_lens') as unknown as IEmbeddable, }) ).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/public/app/actions/utils.ts b/x-pack/plugins/security_solution/public/app/actions/utils.ts index 12c5400dbcf36..d857c54d5091f 100644 --- a/x-pack/plugins/security_solution/public/app/actions/utils.ts +++ b/x-pack/plugins/security_solution/public/app/actions/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public'; +import { isLensApi } from '@kbn/lens-plugin/public'; import type { Serializable } from '@kbn/utility-types'; import { APP_UI_ID } from '../../../common/constants'; @@ -21,8 +21,10 @@ export const isInSecurityApp = (currentAppId?: string): boolean => { return !!currentAppId && currentAppId === APP_UI_ID; }; -export const isLensEmbeddable = (embeddable: IEmbeddable): embeddable is LensEmbeddable => { - return embeddable.type === LENS_EMBEDDABLE_TYPE; +// @TODO: this is a temporary fix. It needs a better refactor on the consumer side here to +// adapt to the new Embeddable architecture +export const isLensEmbeddable = (embeddable: IEmbeddable): embeddable is IEmbeddable => { + return isLensApi(embeddable); }; export const fieldHasCellActions = (field?: string): boolean => { diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.test.tsx index c1d3760813a15..49508dd79b1e1 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.test.tsx @@ -22,32 +22,11 @@ jest.mock('./timeline', () => ({ Timeline: () => <div>{'Timeline'}</div>, })); -jest.mock('../../../common/components/navigation/use_security_solution_navigation', () => { - return { - useSecuritySolutionNavigation: () => ({ - icon: 'logoSecurity', - items: [ - { - id: 'investigate', - name: 'Investigate', - items: [ - { - 'data-href': 'some-data-href', - 'data-test-subj': 'navigation-cases', - disabled: false, - href: 'some-href', - id: 'cases', - isSelected: true, - name: 'Cases', - }, - ], - tabIndex: undefined, - }, - ], - name: 'Security', - }), - }; -}); +const navProps = { icon: 'logoSecurity', items: [], name: 'Security' }; +const mockUseSecuritySolutionNavigation = jest.fn(); +jest.mock('../../../common/components/navigation/use_security_solution_navigation', () => ({ + useSecuritySolutionNavigation: () => mockUseSecuritySolutionNavigation(), +})); const mockUseRouteSpy = jest.fn((): [{ pageName: string }] => [ { pageName: SecurityPageName.alerts }, @@ -69,6 +48,39 @@ const renderComponent = ({ describe('SecuritySolutionTemplateWrapper', () => { beforeEach(() => { jest.clearAllMocks(); + mockUseSecuritySolutionNavigation.mockReturnValue(navProps); + }); + + describe('when navigation props are defined (classic nav)', () => { + beforeEach(() => { + mockUseSecuritySolutionNavigation.mockReturnValue(navProps); + }); + it('should render the children', async () => { + const { queryByText } = renderComponent(); + expect(queryByText('child of wrapper')).toBeInTheDocument(); + }); + }); + + describe('when navigation props are null (project nav)', () => { + beforeEach(() => { + mockUseSecuritySolutionNavigation.mockReturnValue(null); + }); + + it('should render the children', async () => { + const { queryByText } = renderComponent(); + expect(queryByText('child of wrapper')).toBeInTheDocument(); + }); + }); + + describe('when navigation props are undefined (loading)', () => { + beforeEach(() => { + mockUseSecuritySolutionNavigation.mockReturnValue(undefined); + }); + + it('should not render the children', async () => { + const { queryByText } = renderComponent(); + expect(queryByText('child of wrapper')).not.toBeInTheDocument(); + }); }); it('Should render with bottom bar when user allowed', async () => { diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 19e8d55aa2dd5..f547d128ab54b 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -69,8 +69,8 @@ export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateW const { euiTheme, colorMode: globalColorMode } = useEuiTheme(); // There is some logic in the StyledKibanaPageTemplate that checks for children presence, and we dont even need to render the children - // here if isEmptyState is set - const isNotEmpty = !rest.isEmptyState; + // solutionNavProps is momentarily initialized to undefined, this check prevents the children from being re-rendered in the initial load + const renderChildren = !rest.isEmptyState && solutionNavProps !== undefined; /* * StyledKibanaPageTemplate is a styled EuiPageTemplate. Security solution currently passes the header @@ -83,11 +83,11 @@ export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateW theme={euiTheme} $isShowingTimelineOverlay={isShowingTimelineOverlay} paddingSize="none" - solutionNav={solutionNavProps} + solutionNav={solutionNavProps ?? undefined} restrictWidth={false} {...rest} > - {isNotEmpty && ( + {renderChildren && ( <> <GlobalKQLHeader /> <KibanaPageTemplate.Section diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 709bb5f614f7b..1769a805f488f 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -101,6 +101,13 @@ export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exce defaultMessage: 'Shared exception lists', }); +export const SIEM_MIGRATIONS_RULES = i18n.translate( + 'xpack.securitySolution.navigation.siemMigrationsRules', + { + defaultMessage: 'SIEM Rules Migrations', + } +); + export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { defaultMessage: 'Alerts', }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx index 0f59bbbccb8a0..0aed8dc19663b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx @@ -34,6 +34,20 @@ describe('Security Solution Navigation', () => { beforeEach(() => { jest.clearAllMocks(); }); + describe('while chrome style is undefined', () => { + beforeAll(() => { + mockGetChromeStyle$.mockReturnValue(of()); + }); + + it('should return proper navigation props', async () => { + const { result } = renderHook(useSecuritySolutionNavigation); + expect(result.current).toEqual(undefined); + + // check rendering of SecuritySideNav children + expect(mockSecuritySideNav).not.toHaveBeenCalled(); + }); + }); + describe('when classic navigation is enabled', () => { beforeAll(() => { mockGetChromeStyle$.mockReturnValue(of('classic')); @@ -77,9 +91,9 @@ describe('Security Solution Navigation', () => { mockGetChromeStyle$.mockReturnValue(of('project')); }); - it('should return undefined props when disabled', () => { + it('should return null props when disabled', () => { const { result } = renderHook(useSecuritySolutionNavigation); - expect(result.current).toEqual(undefined); + expect(result.current).toEqual(null); }); it('should initialize breadcrumbs', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx index a2f54c04ca467..9abebcbb359ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx @@ -23,16 +23,20 @@ const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mai defaultMessage: 'Security', }); -export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] => { +export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] | null => { const { chrome } = useKibana().services; const chromeStyle$ = useMemo(() => chrome.getChromeStyle$(), [chrome]); - const chromeStyle = useObservable(chromeStyle$, 'classic'); + const chromeStyle = useObservable(chromeStyle$, undefined); useBreadcrumbsNav(); + if (chromeStyle === undefined) { + return undefined; // wait for chromeStyle to be initialized + } + if (chromeStyle === 'project') { - // new shared-ux 'project' navigation enabled, return undefined to disable the 'classic' navigation - return undefined; + // new shared-ux 'project' navigation enabled, return null to disable the 'classic' navigation + return null; } return { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx index 6b264a4dc759f..871750d5ad00f 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx @@ -19,7 +19,6 @@ import type { TypedLensByValueInput, XYState, } from '@kbn/lens-plugin/public'; -import type { LensBaseEmbeddableInput } from '@kbn/lens-plugin/public/embeddable'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { useKibana } from '../../lib/kibana'; import { useLensAttributes } from './use_lens_attributes'; @@ -159,7 +158,7 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({ [dispatch, inputsModelId] ); - const onFilterCallback = useCallback<Required<LensBaseEmbeddableInput>['onFilter']>( + const onFilterCallback = useCallback<Required<TypedLensByValueInput>['onFilter']>( (event) => { if (disableOnClickFilter) { event.preventDefault(); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_embeddable_inspect.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_embeddable_inspect.tsx index ee577d4a310d9..152930fc76498 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_embeddable_inspect.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_embeddable_inspect.tsx @@ -5,14 +5,14 @@ * 2.0. */ -import type { LensBaseEmbeddableInput } from '@kbn/lens-plugin/public/embeddable'; +import type { LensEmbeddableInput } from '@kbn/lens-plugin/public'; import { useCallback } from 'react'; import type { OnEmbeddableLoaded, Request } from './types'; import { getRequestsAndResponses } from './utils'; export const useEmbeddableInspect = (onEmbeddableLoad?: OnEmbeddableLoaded) => { - const setInspectData = useCallback<NonNullable<LensBaseEmbeddableInput['onLoad']>>( + const setInspectData = useCallback<NonNullable<LensEmbeddableInput['onLoad']>>( (isLoading, adapters) => { if (!adapters) { return; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index 8ed7d40519ace..3d6bb712e9d93 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -22,6 +22,7 @@ import { useSourcererDataView } from '../../../sourcerer/containers'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useRouteSpy } from '../../utils/route/use_route_spy'; import { SecurityPageName } from '../../../app/types'; +import type { Query } from '@kbn/es-query'; import { getEventsHistogramLensAttributes } from './lens_attributes/common/events'; jest.mock('../../../sourcerer/containers'); @@ -147,7 +148,7 @@ describe('useLensAttributes', () => { { wrapper } ); - expect(result?.current?.state.query.query).toEqual(''); + expect((result?.current?.state.query as Query).query).toEqual(''); expect(result?.current?.state.filters).toEqual([ ...getExternalAlertLensAttributes().state.filters, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 494cfd5c16b2a..9cc773df320b0 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -17,6 +17,7 @@ import type { LensAttributes, VisualizationEmbeddableProps, } from '../../../common/components/visualization_actions/types'; +import type { Query } from '@kbn/es-query'; const mockVisualizationEmbeddable = jest .fn() @@ -159,7 +160,7 @@ describe('FlyoutRiskSummary', () => { ); const firstColumn = Object.values(datasourceLayers[0].columns)[0]; - expect(lensAttributes.state.query.query).toEqual('host.name: test'); + expect((lensAttributes.state.query as Query).query).toEqual('host.name: test'); expect(firstColumn).toEqual( expect.objectContaining({ sourceField: 'host.risk.calculated_score_norm', @@ -230,7 +231,7 @@ describe('FlyoutRiskSummary', () => { ); const firstColumn = Object.values(datasourceLayers[0].columns)[0]; - expect(lensAttributes.state.query.query).toEqual('user.name: test'); + expect((lensAttributes.state.query as Query).query).toEqual('user.name: test'); expect(firstColumn).toEqual( expect.objectContaining({ sourceField: 'user.risk.calculated_score_norm', diff --git a/x-pack/plugins/security_solution/public/entity_analytics/lens_attributes/risk_score_summary.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/lens_attributes/risk_score_summary.test.ts index 2a00cb1691970..ac7028fa90280 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/lens_attributes/risk_score_summary.test.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/lens_attributes/risk_score_summary.test.ts @@ -12,6 +12,7 @@ import { RiskSeverity } from '../../../common/search_strategy'; import type { MetricVisualizationState } from '@kbn/lens-plugin/public'; import { wrapper } from '../../common/components/visualization_actions/mocks'; import { useLensAttributes } from '../../common/components/visualization_actions/use_lens_attributes'; +import type { Query } from '@kbn/es-query'; jest.mock('../../sourcerer/containers', () => ({ useSourcererDataView: jest.fn().mockReturnValue({ @@ -78,6 +79,6 @@ describe('getRiskScoreSummaryAttributes', () => { { wrapper } ); - expect(result?.current?.state.query.query).toBe(query); + expect((result?.current?.state.query as Query).query).toBe(query); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx index 023b0202ecb63..7ccc3477a9d5d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx @@ -14,6 +14,7 @@ import { NOTES_COUNT_TEST_ID, NOTES_LOADING_TEST_ID, NOTES_TITLE_TEST_ID, + NOTES_VIEW_NOTES_BUTTON_TEST_ID, } from './test_ids'; import { FETCH_NOTES_ERROR, Notes } from './notes'; import { mockContextValue } from '../../shared/mocks/mock_context'; @@ -24,8 +25,10 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelNotesTab } from '../../left'; import { getEmptyValue } from '../../../../common/components/empty_value'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; jest.mock('@kbn/expandable-flyout'); +jest.mock('../../../../common/components/user_privileges'); const mockAddError = jest.fn(); jest.mock('../../../../common/hooks/use_app_toasts', () => ({ @@ -45,6 +48,9 @@ jest.mock('react-redux', () => { describe('<Notes />', () => { beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: true }, + }); jest.clearAllMocks(); }); @@ -297,4 +303,69 @@ describe('<Notes />', () => { title: FETCH_NOTES_ERROR, }); }); + + it('should show View note button if user does not have the correct privileges but notes have already been created', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false }, + }); + + const mockOpenLeftPanel = jest.fn(); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + + const contextValue = { + ...mockContextValue, + eventId: '1', + }; + + const { getByTestId, queryByTestId } = render( + <TestProviders> + <DocumentDetailsContext.Provider value={contextValue}> + <Notes /> + </DocumentDetailsContext.Provider> + </TestProviders> + ); + + expect(mockDispatch).toHaveBeenCalled(); + + expect(getByTestId(NOTES_COUNT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(NOTES_COUNT_TEST_ID)).toHaveTextContent('1'); + + expect(queryByTestId(NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(NOTES_ADD_NOTE_BUTTON_TEST_ID)).not.toBeInTheDocument(); + const button = getByTestId(NOTES_VIEW_NOTES_BUTTON_TEST_ID); + expect(button).toBeInTheDocument(); + + button.click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { tab: LeftPanelNotesTab }, + params: { + id: contextValue.eventId, + indexName: mockContextValue.indexName, + scopeId: mockContextValue.scopeId, + }, + }); + }); + + it('should show a - if user does not have the correct privileges and no notes have been created', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false }, + }); + + const { getByText, queryByTestId } = render( + <TestProviders> + <DocumentDetailsContext.Provider value={mockContextValue}> + <Notes /> + </DocumentDetailsContext.Provider> + </TestProviders> + ); + + expect(mockDispatch).toHaveBeenCalled(); + + expect(queryByTestId(NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(NOTES_ADD_NOTE_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(NOTES_COUNT_TEST_ID)).not.toBeInTheDocument(); + expect(getByText(getEmptyValue())).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx index b0e2008c04103..e10a1ff23919f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { memo, useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { FormattedCount } from '../../../../common/components/formatted_number'; @@ -29,6 +30,7 @@ import { NOTES_COUNT_TEST_ID, NOTES_LOADING_TEST_ID, NOTES_TITLE_TEST_ID, + NOTES_VIEW_NOTES_BUTTON_TEST_ID, } from './test_ids'; import type { State } from '../../../../common/store'; import type { Note } from '../../../../../common/api/timeline'; @@ -55,6 +57,12 @@ export const ADD_NOTE_BUTTON = i18n.translate( defaultMessage: 'Add note', } ); +export const VIEW_NOTES_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.flyout.right.notes.viewNoteButtonAriaLabel', + { + defaultMessage: 'View notes', + } +); /** * Renders a block with the number of notes for the event @@ -64,6 +72,7 @@ export const Notes = memo(() => { const dispatch = useDispatch(); const { eventId, indexName, scopeId, isPreview, isPreviewMode } = useDocumentDetailsContext(); const { addError: addErrorToast } = useAppToasts(); + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const { openLeftPanel } = useExpandableFlyoutApi(); const openExpandedFlyoutNotesTab = useCallback( @@ -101,6 +110,61 @@ export const Notes = memo(() => { } }, [addErrorToast, fetchError, fetchStatus]); + const viewNotesButton = useMemo( + () => ( + <EuiButtonEmpty + onClick={openExpandedFlyoutNotesTab} + size="s" + disabled={isPreviewMode || isPreview} + aria-label={VIEW_NOTES_BUTTON_ARIA_LABEL} + data-test-subj={NOTES_VIEW_NOTES_BUTTON_TEST_ID} + > + <FormattedMessage + id="xpack.securitySolution.flyout.right.notes.viewNoteButtonLabel" + defaultMessage="View {count, plural, one {note} other {notes}}" + values={{ count: notes.length }} + /> + </EuiButtonEmpty> + ), + [isPreview, isPreviewMode, notes.length, openExpandedFlyoutNotesTab] + ); + const addNoteButton = useMemo( + () => ( + <EuiButtonEmpty + iconType="plusInCircle" + onClick={openExpandedFlyoutNotesTab} + size="s" + disabled={isPreviewMode || isPreview} + aria-label={ADD_NOTE_BUTTON} + data-test-subj={NOTES_ADD_NOTE_BUTTON_TEST_ID} + > + {ADD_NOTE_BUTTON} + </EuiButtonEmpty> + ), + [isPreview, isPreviewMode, openExpandedFlyoutNotesTab] + ); + const addNoteButtonIcon = useMemo( + () => ( + <EuiButtonIcon + onClick={openExpandedFlyoutNotesTab} + iconType="plusInCircle" + disabled={isPreviewMode || isPreview || !kibanaSecuritySolutionsPrivileges.crud} + css={css` + margin-left: ${euiTheme.size.xs}; + `} + aria-label={ADD_NOTE_BUTTON} + data-test-subj={NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID} + /> + ), + [ + euiTheme.size.xs, + isPreview, + isPreviewMode, + kibanaSecuritySolutionsPrivileges.crud, + openExpandedFlyoutNotesTab, + ] + ); + return ( <AlertHeaderBlock title={ @@ -120,32 +184,14 @@ export const Notes = memo(() => { ) : ( <> {notes.length === 0 ? ( - <EuiButtonEmpty - iconType="plusInCircle" - onClick={openExpandedFlyoutNotesTab} - size="s" - disabled={isPreviewMode || isPreview} - aria-label={ADD_NOTE_BUTTON} - data-test-subj={NOTES_ADD_NOTE_BUTTON_TEST_ID} - > - {ADD_NOTE_BUTTON} - </EuiButtonEmpty> + <>{kibanaSecuritySolutionsPrivileges.crud ? addNoteButton : getEmptyTagValue()}</> ) : ( <EuiFlexGroup responsive={false} alignItems="center" gutterSize="none"> <EuiFlexItem data-test-subj={NOTES_COUNT_TEST_ID}> <FormattedCount count={notes.length} /> </EuiFlexItem> <EuiFlexItem> - <EuiButtonIcon - onClick={openExpandedFlyoutNotesTab} - iconType="plusInCircle" - disabled={isPreviewMode || isPreview} - css={css` - margin-left: ${euiTheme.size.xs}; - `} - aria-label={ADD_NOTE_BUTTON} - data-test-subj={NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID} - /> + {kibanaSecuritySolutionsPrivileges.crud ? addNoteButtonIcon : viewNotesButton} </EuiFlexItem> </EuiFlexGroup> )} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index 959f8f106bb08..78afeb19e0b80 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -35,6 +35,8 @@ export const CHAT_BUTTON_TEST_ID = 'newChatByTitle' as const; export const NOTES_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesTitle` as const; export const NOTES_ADD_NOTE_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesAddNoteButton` as const; +export const NOTES_VIEW_NOTES_BUTTON_TEST_ID = + `${FLYOUT_HEADER_TEST_ID}NotesViewNotesButton` as const; export const NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesAddNoteIconButton` as const; export const NOTES_COUNT_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesCount` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils/threat_intelligence.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils/threat_intelligence.tsx index 2f5bd6510430b..fefacf3e7fc14 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils/threat_intelligence.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils/threat_intelligence.tsx @@ -7,11 +7,11 @@ import { groupBy, isObject } from 'lodash'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { getDataFromFieldsHits } from '@kbn/timelines-plugin/common'; import { i18n } from '@kbn/i18n'; import type { ThreatDetailsRow } from '../../left/components/threat_details_view_enrichment_accordion'; import type { CtiEnrichment, EventFields } from '../../../../../common/search_strategy'; import { isValidEventField } from '../../../../../common/search_strategy'; -import { getDataFromFieldsHits } from '../../../../../common/utils/field_formatters'; import { DEFAULT_INDICATOR_SOURCE_PATH, ENRICHMENT_DESTINATION_PATH, diff --git a/x-pack/plugins/security_solution/public/kubernetes/routes.tsx b/x-pack/plugins/security_solution/public/kubernetes/routes.tsx index 67bd4a9fcad85..7520de8cffa5a 100644 --- a/x-pack/plugins/security_solution/public/kubernetes/routes.tsx +++ b/x-pack/plugins/security_solution/public/kubernetes/routes.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import { allowedExperimentalValues } from '../../common'; import { KubernetesContainer } from './pages'; import type { SecuritySubPluginRoutes } from '../app/types'; @@ -24,7 +25,7 @@ export const KubernetesRoutes = () => ( export const routes: SecuritySubPluginRoutes = [ { - path: KUBERNETES_PATH, + path: allowedExperimentalValues.kubernetesEnabled ? KUBERNETES_PATH : [], component: KubernetesRoutes, }, ]; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 8bbba3885a2ab..1be423c988397 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -29,6 +29,7 @@ import { EntityAnalytics } from './entity_analytics'; import { Assets } from './assets'; import { Investigations } from './investigations'; import { MachineLearning } from './machine_learning'; +import { SiemMigrations } from './siem_migrations'; /** * The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import. @@ -53,5 +54,6 @@ const subPluginClasses = { Assets, Investigations, MachineLearning, + SiemMigrations, }; export { subPluginClasses }; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx index 5821dd349749d..4c0796dacd3b6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx @@ -31,17 +31,21 @@ jest.mock('../../../../common/containers/use_global_time', () => { }); jest.mock('../../../../common/lib/kibana'); -const mockGetCasesStatus = jest.fn(); -mockGetCasesStatus.mockResolvedValue({ - countOpenCases: 1, - countInProgressCases: 2, - countClosedCases: 3, +const mockGetCasesMetrics = jest.fn(); +mockGetCasesMetrics.mockResolvedValue({ + status: { + open: 1, + inProgress: 2, + closed: 3, + }, }); -mockGetCasesStatus.mockResolvedValueOnce({ - countOpenCases: 0, - countInProgressCases: 0, - countClosedCases: 0, +mockGetCasesMetrics.mockResolvedValueOnce({ + status: { + open: 0, + inProgress: 0, + closed: 0, + }, }); const mockUseKibana = { @@ -50,7 +54,7 @@ const mockUseKibana = { ...mockCasesContract(), api: { cases: { - getCasesStatus: mockGetCasesStatus, + getCasesMetrics: mockGetCasesMetrics, }, }, }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx index 93b755f710879..55ac6ce45abd9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import type { CasesStatus } from '@kbn/cases-plugin/common'; +import type { CasesMetricsResponse } from '@kbn/cases-plugin/common'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common'; import { useState, useEffect, useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { APP_ID } from '../../../../../common/constants'; @@ -34,15 +35,18 @@ export const useCasesByStatus = ({ skip = false }) => { const uniqueQueryId = useMemo(() => `useCaseItems-${uuidv4()}`, []); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [isLoading, setIsLoading] = useState(true); - const [casesCounts, setCasesCounts] = useState<CasesStatus | null>(null); + const [casesCounts, setCasesCounts] = useState<CasesMetricsResponse['status'] | null | undefined>( + null + ); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); const fetchCases = async () => { try { - const casesResponse = await cases.api.cases.getCasesStatus( + const casesResponse = await cases.api.cases.getCasesMetrics( { + features: [CaseMetricsFeature.STATUS], from, to, owner: APP_ID, @@ -51,7 +55,7 @@ export const useCasesByStatus = ({ skip = false }) => { ); if (isSubscribed) { - setCasesCounts(casesResponse); + setCasesCounts(casesResponse.status); } } catch (error) { if (isSubscribed) { @@ -88,14 +92,12 @@ export const useCasesByStatus = ({ skip = false }) => { }, [cases.api.cases, from, skip, to, setQuery, deleteQuery, uniqueQueryId]); return { - closed: casesCounts?.countClosedCases ?? 0, - inProgress: casesCounts?.countInProgressCases ?? 0, + closed: casesCounts?.closed ?? 0, + inProgress: casesCounts?.inProgress ?? 0, isLoading, - open: casesCounts?.countOpenCases ?? 0, + open: casesCounts?.open ?? 0, totalCounts: - (casesCounts?.countClosedCases ?? 0) + - (casesCounts?.countInProgressCases ?? 0) + - (casesCounts?.countOpenCases ?? 0), + (casesCounts?.closed ?? 0) + (casesCounts?.inProgress ?? 0) + (casesCounts?.open ?? 0), updatedAt, }; }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b74d0cffdc88d..f933832264247 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -245,6 +245,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S assets: new subPluginClasses.Assets(), investigations: new subPluginClasses.Investigations(), machineLearning: new subPluginClasses.MachineLearning(), + siemMigrations: new subPluginClasses.SiemMigrations(), }; } return this._subPlugins; @@ -279,6 +280,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S assets: subPlugins.assets.start(), investigations: subPlugins.investigations.start(), machineLearning: subPlugins.machineLearning.start(), + siemMigrations: subPlugins.siemMigrations.start( + this.experimentalFeatures.siemMigrationsEnabled + ), }; } diff --git a/x-pack/plugins/security_solution/public/plugin_services.ts b/x-pack/plugins/security_solution/public/plugin_services.ts index 97b9a163b9f80..92b4bc586a5b6 100644 --- a/x-pack/plugins/security_solution/public/plugin_services.ts +++ b/x-pack/plugins/security_solution/public/plugin_services.ts @@ -19,6 +19,7 @@ import type { ConfigSettings } from '../common/config_settings'; import { parseConfigSettings } from '../common/config_settings'; import { APP_UI_ID } from '../common/constants'; import { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service'; +import { createSiemMigrationsService } from './siem_migrations/service'; import type { SecuritySolutionUiConfigType } from './common/types'; import type { PluginStart, @@ -152,6 +153,7 @@ export class PluginServices { customDataService, timelineDataService, topValuesPopover: new TopValuesPopoverService(), + siemMigrations: await createSiemMigrationsService(coreStart), ...(params && { onAppLeave: params.onAppLeave, setHeaderActionMenu: params.setHeaderActionMenu, diff --git a/x-pack/plugins/security_solution/public/rules/links.ts b/x-pack/plugins/security_solution/public/rules/links.ts index 5564a8b9b4e2a..ba4280739bbe6 100644 --- a/x-pack/plugins/security_solution/public/rules/links.ts +++ b/x-pack/plugins/security_solution/public/rules/links.ts @@ -29,6 +29,7 @@ import type { LinkItem } from '../common/links'; import { IconConsoleCloud } from '../common/icons/console_cloud'; import { IconRollup } from '../common/icons/rollup'; import { IconDashboards } from '../common/icons/dashboards'; +import { siemMigrationsLinks } from '../siem_migrations/links'; export const links: LinkItem = { id: SecurityPageName.rulesLanding, @@ -106,6 +107,7 @@ export const links: LinkItem = { }), ], }, + siemMigrationsLinks, ], categories: [ { @@ -116,6 +118,7 @@ export const links: LinkItem = { SecurityPageName.rules, SecurityPageName.cloudSecurityPostureBenchmarks, SecurityPageName.exceptions, + SecurityPageName.siemMigrationsRules, ], }, { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/index.ts new file mode 100644 index 0000000000000..4b842e2a8a56c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SecuritySubPlugin } from '../app/types'; +import { routes } from './routes'; + +export class SiemMigrations { + public setup() {} + + public start(isEnabled = false): SecuritySubPlugin { + return { + routes: isEnabled ? routes : [], + }; + } +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/jest.config.js b/x-pack/plugins/security_solution/public/siem_migrations/jest.config.js new file mode 100644 index 0000000000000..fd313059456a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['<rootDir>/x-pack/plugins/security_solution/public/siem_migrations'], + coverageDirectory: + '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/siem_migrations', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '<rootDir>/x-pack/plugins/security_solution/public/siem_migrations/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/links.ts b/x-pack/plugins/security_solution/public/siem_migrations/links.ts new file mode 100644 index 0000000000000..34db8a357785a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/links.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 { i18n } from '@kbn/i18n'; +import { + SecurityPageName, + SERVER_APP_ID, + SIEM_MIGRATIONS_RULES_PATH, +} from '../../common/constants'; +import { SIEM_MIGRATIONS_RULES } from '../app/translations'; +import type { LinkItem } from '../common/links/types'; +import { IconConsoleCloud } from '../common/icons/console_cloud'; + +export const siemMigrationsLinks: LinkItem = { + id: SecurityPageName.siemMigrationsRules, + title: SIEM_MIGRATIONS_RULES, + description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesDescription', { + defaultMessage: 'SIEM Rules Migrations.', + }), + landingIcon: IconConsoleCloud, + path: SIEM_MIGRATIONS_RULES_PATH, + capabilities: [`${SERVER_APP_ID}.show`], + skipUrlState: true, + hideTimeline: true, + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRules', { + defaultMessage: 'SIEM Rules Migrations', + }), + ], + experimentalKey: 'siemMigrationsEnabled', +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx b/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx new file mode 100644 index 0000000000000..cc2b9fbad3451 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/routes.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Routes, Route } from '@kbn/shared-ux-router'; + +import type { SecuritySubPluginRoutes } from '../app/types'; +import { SIEM_MIGRATIONS_RULES_PATH, SecurityPageName } from '../../common/constants'; +import { RulesPage } from './rules/pages'; +import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; +import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; + +export const RulesRoutes = () => { + return ( + <PluginTemplateWrapper> + <SecurityRoutePageWrapper pageName={SecurityPageName.siemMigrationsRules}> + <Routes> + <Route path={`${SIEM_MIGRATIONS_RULES_PATH}/:migrationId?`} component={RulesPage} /> + </Routes> + </SecurityRoutePageWrapper> + </PluginTemplateWrapper> + ); +}; + +export const routes: SecuritySubPluginRoutes = [ + { + path: SIEM_MIGRATIONS_RULES_PATH, + component: RulesRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts new file mode 100644 index 0000000000000..7232cb722bd1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { replaceParams } from '@kbn/openapi-common/shared'; + +import { KibanaServices } from '../../../common/lib/kibana'; + +import { + SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + SIEM_RULE_MIGRATION_PATH, +} from '../../../../common/siem_migrations/constants'; +import type { + GetAllStatsRuleMigrationResponse, + GetRuleMigrationResponse, +} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; + +/** + * Retrieves the stats for all the existing migrations, aggregated by `migration_id`. + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleMigrationsStatsAll = async ({ + signal, +}: { + signal: AbortSignal | undefined; +}): Promise<GetAllStatsRuleMigrationResponse> => { + return KibanaServices.get().http.fetch<GetAllStatsRuleMigrationResponse>( + SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + { + method: 'GET', + version: '1', + signal, + } + ); +}; + +/** + * Retrieves all the migration rule documents of a specific migration. + * + * @param migrationId `id` of the migration to retrieve rule documents for + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleMigrations = async ({ + migrationId, + signal, +}: { + migrationId: string; + signal: AbortSignal | undefined; +}): Promise<GetRuleMigrationResponse> => { + return KibanaServices.get().http.fetch<GetRuleMigrationResponse>( + replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), + { + method: 'GET', + version: '1', + signal, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts new file mode 100644 index 0000000000000..61e0d1e05f7f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const ONE_MINUTE = 60000; + +export const DEFAULT_QUERY_OPTIONS = { + refetchIntervalInBackground: false, + staleTime: ONE_MINUTE * 5, +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts new file mode 100644 index 0000000000000..76cf01c6c35d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.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 type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { replaceParams } from '@kbn/openapi-common/shared'; +import { DEFAULT_QUERY_OPTIONS } from './constants'; +import { getRuleMigrations } from '../api'; +import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; + +export const useGetRuleMigrationsQuery = ( + migrationId: string, + options?: UseQueryOptions<GetRuleMigrationResponse> +) => { + const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, { + migration_id: migrationId, + }); + return useQuery<GetRuleMigrationResponse>( + ['GET', SPECIFIC_MIGRATION_PATH], + async ({ signal }) => { + return getRuleMigrations({ migrationId, signal }); + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx new file mode 100644 index 0000000000000..ba73bd9c84946 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx @@ -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 React, { useMemo } from 'react'; + +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface HeaderButtonsProps { + /** + * Available rule migrations ids + */ + migrationsIds: string[]; + + /** + * Selected rule migration id + */ + selectedMigrationId: string | undefined; + + /** + * Handles migration selection changes + * @param selectedId Selected migration id + * @returns + */ + onMigrationIdChange: (selectedId?: string) => void; +} + +const HeaderButtonsComponent: React.FC<HeaderButtonsProps> = ({ + migrationsIds, + selectedMigrationId, + onMigrationIdChange, +}) => { + const migrationOptions = useMemo(() => { + const options: Array<EuiComboBoxOptionOption<string>> = migrationsIds.map((id, index) => ({ + value: id, + 'data-test-subj': `migrationSelectionOption-${index}`, + label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1), + })); + return options; + }, [migrationsIds]); + const selectedMigrationOption = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => { + const index = migrationsIds.findIndex((id) => id === selectedMigrationId); + return index !== -1 + ? [ + { + value: selectedMigrationId, + 'data-test-subj': `migrationSelectionOption-${index}`, + label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(index + 1), + }, + ] + : []; + }, [migrationsIds, selectedMigrationId]); + + const onChange = (selected: Array<EuiComboBoxOptionOption<string>>) => { + onMigrationIdChange(selected[0].value); + }; + + if (!migrationsIds.length) { + return null; + } + + return ( + <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> + <EuiFlexItem grow={false}> + <EuiComboBox + aria-label={i18n.SIEM_MIGRATIONS_OPTION_AREAL_LABEL} + onChange={onChange} + options={migrationOptions} + selectedOptions={selectedMigrationOption} + singleSelection={{ asPlainText: true }} + isClearable={false} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const HeaderButtons = React.memo(HeaderButtonsComponent); +HeaderButtons.displayName = 'HeaderButtons'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts new file mode 100644 index 0000000000000..e00721c70103a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SIEM_MIGRATIONS_OPTION_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel', + { + defaultMessage: 'Select a migration', + } +); + +export const SIEM_MIGRATIONS_OPTION_LABEL = (optionIndex: number) => + i18n.translate('xpack.securitySolution.siemMigrations.rules.selectionOption.title', { + defaultMessage: 'SIEM rule migration {optionIndex}', + values: { + optionIndex, + }, + }); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx new file mode 100644 index 0000000000000..5f4ae3098b6a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup } from '@elastic/eui'; +import type { Dispatch, SetStateAction } from 'react'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { RuleSearchField } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_search_field'; +import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; + +const FilterWrapper = styled(EuiFlexGroup)` + margin-bottom: ${({ theme }) => theme.eui.euiSizeM}; +`; + +export interface FiltersComponentProps { + /** + * Currently selected table filter + */ + filterOptions: TableFilterOptions; + + /** + * Handles filter options changes + */ + setFilterOptions: Dispatch<SetStateAction<TableFilterOptions>>; +} + +/** + * Collection of filters for filtering data within the SIEM Rules Migrations table. + * Contains search bar and tag selection + */ +const FiltersComponent: React.FC<FiltersComponentProps> = ({ filterOptions, setFilterOptions }) => { + const handleOnSearch = useCallback( + (filterString: string) => { + setFilterOptions((filters) => ({ + ...filters, + filter: filterString.trim(), + })); + }, + [setFilterOptions] + ); + + return ( + <FilterWrapper gutterSize="m" justifyContent="flexEnd" wrap> + <RuleSearchField + initialValue={filterOptions.filter} + onSearch={handleOnSearch} + placeholder={i18n.SEARCH_PLACEHOLDER} + /> + </FilterWrapper> + ); +}; + +export const Filters = React.memo(FiltersComponent); +Filters.displayName = 'Filters'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx new file mode 100644 index 0000000000000..0cd3e07ea11a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.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 { + EuiInMemoryTable, + EuiSkeletonLoading, + EuiProgress, + EuiSkeletonTitle, + EuiSkeletonText, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React, { useState } from 'react'; + +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { + RULES_TABLE_INITIAL_PAGE_SIZE, + RULES_TABLE_PAGE_SIZE_OPTIONS, +} from '../../../../detection_engine/rule_management_ui/components/rules_table/constants'; +import { NoItemsMessage } from './no_items_message'; +import { Filters } from './filters'; +import { useRulesTableColumns } from '../../hooks/use_rules_table_columns'; +import { useGetRuleMigrationsQuery } from '../../api/hooks/use_get_rule_migrations'; +import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; +import { useFilterRulesToInstall } from '../../hooks/use_filter_rules_to_install'; + +export interface RulesTableComponentProps { + /** + * Selected rule migration id + */ + migrationId: string; + + /** + * Opens the flyout with the details of the rule migration + * @param rule Rule migration + * @returns + */ + openRulePreview: (rule: RuleMigration) => void; +} + +/** + * Table Component for displaying SIEM rules migrations + */ +const RulesTableComponent: React.FC<RulesTableComponentProps> = ({ + migrationId, + openRulePreview, +}) => { + const { data: ruleMigrations, isLoading } = useGetRuleMigrationsQuery(migrationId); + + const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]); + + const [filterOptions, setFilterOptions] = useState<TableFilterOptions>({ + filter: '', + }); + + const filteredRuleMigrations = useFilterRulesToInstall({ + filterOptions, + ruleMigrations: ruleMigrations ?? [], + }); + + const shouldShowProgress = isLoading; + + const rulesColumns = useRulesTableColumns({ + openRulePreview, + }); + + return ( + <> + {shouldShowProgress && ( + <EuiProgress + data-test-subj="loadingRulesInfoProgress" + size="xs" + position="absolute" + color="accent" + /> + )} + <EuiSkeletonLoading + isLoading={isLoading} + loadingContent={ + <> + <EuiSkeletonTitle /> + <EuiSkeletonText /> + </> + } + loadedContent={ + !filteredRuleMigrations.length ? ( + <NoItemsMessage /> + ) : ( + <> + <EuiFlexGroup direction="column"> + <EuiFlexItem grow={false}> + <Filters filterOptions={filterOptions} setFilterOptions={setFilterOptions} /> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiInMemoryTable + items={filteredRuleMigrations} + sorting + pagination={{ + initialPageSize: RULES_TABLE_INITIAL_PAGE_SIZE, + pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS, + }} + selection={{ + selectable: () => true, + onSelectionChange: setSelectedRuleMigrations, + initialSelected: selectedRuleMigrations, + }} + itemId="rule_id" + data-test-subj="rules-translation-table" + columns={rulesColumns} + /> + </> + ) + } + /> + </> + ); +}; + +export const RulesTable = React.memo(RulesTableComponent); +RulesTable.displayName = 'RulesTable'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx new file mode 100644 index 0000000000000..7aeaac7ab2f6b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/no_items_message.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { SecurityPageName } from '../../../../../common'; +import { useGetSecuritySolutionLinkProps } from '../../../../common/components/links'; +import * as i18n from './translations'; + +const NoItemsMessageComponent = () => { + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); + const { onClick: onClickLink } = getSecuritySolutionLinkProps({ + deepLinkId: SecurityPageName.landing, + path: 'siem_migrations', + }); + + return ( + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} + direction="column" + wrap={true} + > + <EuiFlexItem grow={false}> + <EuiEmptyPrompt + title={<h2>{i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL}</h2>} + titleSize="s" + body={i18n.NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY} + data-test-subj="noRulesTranslationAvailableForInstall" + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + iconType="arrowLeft" + color={'primary'} + onClick={onClickLink} + data-test-subj="goToSiemMigrationsButton" + > + {i18n.GO_BACK_TO_RULES_TABLE_BUTTON} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const NoItemsMessage = React.memo(NoItemsMessageComponent); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts new file mode 100644 index 0000000000000..812f26f628e49 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.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 { i18n } from '@kbn/i18n'; + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.searchBarPlaceholder', + { + defaultMessage: 'Search by rule name', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.noRulesTitle', + { + defaultMessage: 'Empty migration', + } +); + +export const NO_TRANSLATIONS_AVAILABLE_FOR_INSTALL_BODY = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.noRulesBodyTitle', + { + defaultMessage: 'There are no translations available for installation', + } +); + +export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.goToMigrationsPageButton', + { + defaultMessage: 'Go back to SIEM Migrations', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx new file mode 100644 index 0000000000000..aaf256cfb60b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { StatusBadge } from '.'; + +describe('StatusBadge', () => { + it('renders correctly', () => { + const wrapper = shallow(<StatusBadge value="full" />); + + expect(wrapper.find('HealthTruncateText')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx new file mode 100644 index 0000000000000..40b3c5ceb5719 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.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 { euiLightVars } from '@kbn/ui-theme'; + +import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; +import { convertTranslationResultIntoText } from '../../utils/helpers'; + +const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars; +const statusToColorMap: Record<RuleMigrationTranslationResult, string> = { + full: euiColorVis0, + partial: euiColorVis7, + untranslatable: euiColorVis9, +}; + +interface Props { + value?: RuleMigrationTranslationResult; + installedRuleId?: string; + 'data-test-subj'?: string; +} + +const StatusBadgeComponent: React.FC<Props> = ({ + value, + installedRuleId, + 'data-test-subj': dataTestSubj = 'translation-result', +}) => { + const translationResult = installedRuleId || !value ? 'full' : value; + const displayValue = convertTranslationResultIntoText(translationResult); + const color = statusToColorMap[translationResult]; + + return ( + <HealthTruncateText + healthColor={color} + tooltipContent={displayValue} + dataTestSubj={dataTestSubj} + > + {displayValue} + </HealthTruncateText> + ); +}; + +export const StatusBadge = React.memo(StatusBadgeComponent); +StatusBadge.displayName = 'StatusBadge'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts new file mode 100644 index 0000000000000..4d6bcd542b866 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%']; +export const LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['30%', '70%']; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx new file mode 100644 index 0000000000000..4aaff21281d64 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { FC, PropsWithChildren } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + EuiButtonEmpty, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTabbedContent, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; + +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { + RuleOverviewTab, + useOverviewTabSections, +} from '../../../../detection_engine/rule_management/components/rule_details/rule_overview_tab'; +import { + type RuleResponse, + type Severity, +} from '../../../../../common/api/detection_engine/model/rule_schema'; + +import * as i18n from './translations'; +import { + DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, + LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS, +} from './constants'; +import { TranslationTab } from './translation_tab'; +import { + DEFAULT_TRANSLATION_RISK_SCORE, + DEFAULT_TRANSLATION_SEVERITY, +} from '../../utils/constants'; + +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + .euiFlyoutBody__overflow { + display: flex; + flex: 1; + overflow: hidden; + + .euiFlyoutBody__overflowContent { + flex: 1; + overflow: hidden; + padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} 0`}; + } + } +`; + +const StyledFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const StyledEuiFlexItem = styled(EuiFlexItem)` + &.euiFlexItem { + flex: 1 0 0; + overflow: hidden; + } +`; + +const StyledEuiTabbedContent = styled(EuiTabbedContent)` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + + > [role='tabpanel'] { + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + overflow-y: auto; + + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + } + } +`; + +/* + * Fixes tabs to the top and allows the content to scroll. + */ +const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => ( + <StyledFlexGroup direction="column" gutterSize="none"> + <StyledEuiFlexItem grow={true}> + <StyledEuiTabbedContent {...props} /> + </StyledEuiFlexItem> + </StyledFlexGroup> +); + +const tabPaddingClassName = css` + padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM}; +`; + +export const TabContentPadding: FC<PropsWithChildren<unknown>> = ({ children }) => ( + <div className={tabPaddingClassName}>{children}</div> +); + +interface TranslationDetailsFlyoutProps { + ruleActions?: React.ReactNode; + ruleMigration: RuleMigration; + size?: EuiFlyoutProps['size']; + extraTabs?: EuiTabbedContentTab[]; + closeFlyout: () => void; +} + +export const TranslationDetailsFlyout = ({ + ruleActions, + ruleMigration, + size = 'm', + extraTabs = [], + closeFlyout, +}: TranslationDetailsFlyoutProps) => { + const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections(); + + const rule: RuleResponse = useMemo(() => { + const esqlLanguage = ruleMigration.elastic_rule?.query_language ?? 'esql'; + return { + type: esqlLanguage, + language: esqlLanguage, + name: ruleMigration.elastic_rule?.title, + description: ruleMigration.elastic_rule?.description, + query: ruleMigration.elastic_rule?.query, + + // Default values + severity: (ruleMigration.elastic_rule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY, + risk_score: DEFAULT_TRANSLATION_RISK_SCORE, + from: 'now-360s', + to: 'now', + interval: '5m', + } as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter + }, [ruleMigration]); + + const translationTab: EuiTabbedContentTab = useMemo( + () => ({ + id: 'translation', + name: i18n.TRANSLATION_TAB_LABEL, + content: ( + <TabContentPadding> + <TranslationTab ruleMigration={ruleMigration} /> + </TabContentPadding> + ), + }), + [ruleMigration] + ); + + const overviewTab: EuiTabbedContentTab = useMemo( + () => ({ + id: 'overview', + name: i18n.OVERVIEW_TAB_LABEL, + content: ( + <TabContentPadding> + <RuleOverviewTab + rule={rule} + columnWidths={ + size === 'l' + ? LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS + : DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS + } + expandedOverviewSections={expandedOverviewSections} + toggleOverviewSection={toggleOverviewSection} + /> + </TabContentPadding> + ), + }), + [rule, size, expandedOverviewSections, toggleOverviewSection] + ); + + const tabs = useMemo(() => { + return [...extraTabs, translationTab, overviewTab]; + }, [extraTabs, translationTab, overviewTab]); + + const [selectedTabId, setSelectedTabId] = useState<string>(tabs[0].id); + const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0]; + + useEffect(() => { + if (!tabs.find((tab) => tab.id === selectedTabId)) { + // Switch to first tab if currently selected tab is not available for this rule + setSelectedTabId(tabs[0].id); + } + }, [tabs, selectedTabId]); + + const onTabClick = (tab: EuiTabbedContentTab) => { + setSelectedTabId(tab.id); + }; + + const migrationsRulesFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'migrationRulesFlyoutTitle', + }); + + return ( + <EuiFlyout + size={size} + onClose={closeFlyout} + key="migrations-rules-flyout" + paddingSize="l" + data-test-subj="ruleMigrationDetailsFlyout" + aria-labelledby={migrationsRulesFlyoutTitleId} + ownFocus + > + <EuiFlyoutHeader> + <EuiTitle size="m"> + <h2 id={migrationsRulesFlyoutTitleId}>{rule.name}</h2> + </EuiTitle> + <EuiSpacer size="l" /> + </EuiFlyoutHeader> + <StyledEuiFlyoutBody> + <ScrollableFlyoutTabbedContent + tabs={tabs} + selectedTab={selectedTab} + onTabClick={onTabClick} + /> + </StyledEuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={closeFlyout} flush="left"> + {i18n.DISMISS_BUTTON_LABEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}>{ruleActions}</EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx new file mode 100644 index 0000000000000..57e99440e60a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/header.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import * as i18n from './translations'; + +export function TranslationTabHeader(): JSX.Element { + return ( + <EuiFlexGroup direction="row" alignItems="center"> + <EuiTitle data-test-subj="ruleTranslationLabel" size="xs"> + <h5>{i18n.TAB_HEADER_TITLE}</h5> + </EuiTitle> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx new file mode 100644 index 0000000000000..66836b8ea5631 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/index.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiAccordion, + EuiBadge, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSplitPanel, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { TranslationTabHeader } from './header'; +import { RuleQueryComponent } from './rule_query'; +import * as i18n from './translations'; +import { convertTranslationResultIntoText } from '../../../utils/helpers'; + +interface TranslationTabProps { + ruleMigration: RuleMigration; +} + +export const TranslationTab = ({ ruleMigration }: TranslationTabProps) => { + const { euiTheme } = useEuiTheme(); + + const name = ruleMigration.elastic_rule?.title ?? ruleMigration.original_rule.title; + const originalQuery = ruleMigration.original_rule.query; + const elasticQuery = ruleMigration.elastic_rule?.query ?? 'Prebuilt rule query'; + + return ( + <> + <EuiSpacer size="m" /> + <EuiFormRow label={i18n.NAME_LABEL} fullWidth> + <EuiFieldText value={name} fullWidth /> + </EuiFormRow> + <EuiSpacer size="m" /> + <EuiAccordion + id="translationQueryItem" + buttonContent={<TranslationTabHeader />} + initialIsOpen={true} + > + <EuiFlexItem> + <EuiSpacer size="s" /> + <EuiSplitPanel.Outer grow hasShadow={false} hasBorder={true}> + <EuiSplitPanel.Inner grow={false} color="subdued" paddingSize="s"> + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiTitle size="xxs"> + <h2> + <FormattedMessage + id="xpack.securitySolution.detectionEngine.translationDetails.translationTab.statusTitle" + defaultMessage="Translation status" + /> + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiBadge + iconSide="right" + iconType="arrowDown" + color="primary" + onClick={() => {}} + onClickAriaLabel={'Click to update translation status'} + > + {convertTranslationResultIntoText(ruleMigration.translation_result)} + </EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + </EuiSplitPanel.Inner> + <EuiSplitPanel.Inner grow> + <EuiFlexGroup gutterSize="s" alignItems="flexStart"> + <EuiFlexItem grow={1}> + <RuleQueryComponent + title={i18n.SPLUNK_QUERY_TITLE} + query={originalQuery} + canEdit={false} + /> + </EuiFlexItem> + <EuiFlexItem + grow={0} + css={css` + align-self: stretch; + border-right: ${euiTheme.border.thin}; + `} + /> + <EuiFlexItem grow={1}> + <RuleQueryComponent + title={i18n.ESQL_TRANSLATION_TITLE} + query={elasticQuery} + canEdit={false} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiSplitPanel.Inner> + </EuiSplitPanel.Outer> + </EuiFlexItem> + </EuiAccordion> + </> + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx new file mode 100644 index 0000000000000..50977cafb18d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/rule_query.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiMarkdownEditor, EuiMarkdownFormat, EuiTitle } from '@elastic/eui'; +import { SideHeader } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/components/side_header'; +import { FinalSideHelpInfo } from '../../../../../detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info'; +import * as i18n from './translations'; + +interface RuleQueryProps { + title: string; + query: string; + canEdit?: boolean; +} + +export const RuleQueryComponent = ({ title, query, canEdit }: RuleQueryProps) => { + const queryTextComponent = useMemo(() => { + if (canEdit) { + return ( + <EuiMarkdownEditor + aria-label={i18n.TRANSLATED_QUERY_AREAL_LABEL} + value={query} + onChange={() => {}} + height={400} + initialViewMode={'viewing'} + /> + ); + } else { + return <EuiMarkdownFormat>{query}</EuiMarkdownFormat>; + } + }, [canEdit, query]); + return ( + <> + <SideHeader> + <EuiTitle size="xxs"> + <h3> + {title} + <FinalSideHelpInfo /> + </h3> + </EuiTitle> + </SideHeader> + {queryTextComponent} + </> + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts new file mode 100644 index 0000000000000..e7532a5a8b2e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translation_tab/translations.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TAB_HEADER_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.title', + { + defaultMessage: 'Translation', + } +); + +export const NAME_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.nameLabel', + { + defaultMessage: 'Name', + } +); + +export const SPLUNK_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.splunkQueryTitle', + { + defaultMessage: 'Splunk query', + } +); + +export const ESQL_TRANSLATION_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.esqlTranslationTitle', + { + defaultMessage: 'ES|QL translation', + } +); + +export const TRANSLATED_QUERY_AREAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.queryArealLabel', + { + defaultMessage: 'Translated query', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.ts new file mode 100644 index 0000000000000..8e6582b8c198e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/translations.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 { i18n } from '@kbn/i18n'; + +export const OVERVIEW_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.overviewTabLabel', + { + defaultMessage: 'Overview', + } +); + +export const TRANSLATION_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTabLabel', + { + defaultMessage: 'Translation', + } +); + +export const DISMISS_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.dismissButtonLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/index.tsx new file mode 100644 index 0000000000000..0a33869eff418 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +const UnknownMigrationComponent = () => { + return ( + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} + direction="column" + wrap={true} + > + <EuiFlexItem grow={false}> + <EuiEmptyPrompt + title={<h2>{i18n.UNKNOWN_MIGRATION}</h2>} + titleSize="s" + body={i18n.UNKNOWN_MIGRATION_BODY} + data-test-subj="noMigrationsAvailable" + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const UnknownMigration = React.memo(UnknownMigrationComponent); +UnknownMigration.displayName = 'UnknownMigration'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/translations.ts new file mode 100644 index 0000000000000..8720640858b94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/unknown_migration/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNKNOWN_MIGRATION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.unknownMigrationTitle', + { + defaultMessage: 'Unknown migration', + } +); + +export const UNKNOWN_MIGRATION_BODY = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.unknownMigrationBodyTitle', + { + defaultMessage: + 'Selected migration does not exist. Please select one of the available migraitons', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts new file mode 100644 index 0000000000000..74845b5f257ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.columns.statusTitle', + { + defaultMessage: 'Status', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.ts new file mode 100644 index 0000000000000..f6862d3d90380 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_filter_rules_to_install.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 { useMemo } from 'react'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { FilterOptions } from '../../../detection_engine/rule_management/logic/types'; + +export type TableFilterOptions = Pick<FilterOptions, 'filter'>; + +export const useFilterRulesToInstall = ({ + ruleMigrations, + filterOptions, +}: { + ruleMigrations: RuleMigration[]; + filterOptions: TableFilterOptions; +}) => { + const filteredRules = useMemo(() => { + const { filter } = filterOptions; + return ruleMigrations.filter((migration) => { + const name = migration.elastic_rule?.title ?? migration.original_rule.title; + if (filter && !name.toLowerCase().includes(filter.toLowerCase())) { + return false; + } + return true; + }); + }, [filterOptions, ruleMigrations]); + + return filteredRules; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts new file mode 100644 index 0000000000000..c681af0d2a21c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import useObservable from 'react-use/lib/useObservable'; +import { useEffect, useMemo } from 'react'; +import { useKibana } from '../../../common/lib/kibana'; + +export const useLatestStats = () => { + const { siemMigrations } = useKibana().services; + + useEffect(() => { + siemMigrations.rules.startPolling(); + }, [siemMigrations.rules]); + + const latestStats$ = useMemo(() => siemMigrations.rules.getLatestStats$(), [siemMigrations]); + const latestStats = useObservable(latestStats$, null); + + return { data: latestStats ?? [], isLoading: latestStats === null }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.tsx new file mode 100644 index 0000000000000..1721b4e280aad --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rule_preview_flyout.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 type { ReactNode } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; +import type { EuiTabbedContentTab } from '@elastic/eui'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { TranslationDetailsFlyout } from '../components/translation_details_flyout'; + +interface UseRulePreviewFlyoutParams { + ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode; + extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; +} + +interface UseRulePreviewFlyoutResult { + rulePreviewFlyout: ReactNode; + openRulePreview: (rule: RuleMigration) => void; + closeRulePreview: () => void; +} + +export function useRulePreviewFlyout({ + extraTabsFactory, + ruleActionsFactory, +}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult { + const [ruleMigration, setRuleMigrationForPreview] = useState<RuleMigration | undefined>(); + const closeRulePreview = useCallback(() => setRuleMigrationForPreview(undefined), []); + const ruleActions = useMemo( + () => ruleMigration && ruleActionsFactory(ruleMigration, closeRulePreview), + [ruleMigration, ruleActionsFactory, closeRulePreview] + ); + const extraTabs = useMemo( + () => (ruleMigration && extraTabsFactory ? extraTabsFactory(ruleMigration) : []), + [ruleMigration, extraTabsFactory] + ); + + return { + rulePreviewFlyout: ruleMigration && ( + <TranslationDetailsFlyout + ruleMigration={ruleMigration} + size="l" + closeFlyout={closeRulePreview} + ruleActions={ruleActions} + extraTabs={extraTabs} + /> + ), + openRulePreview: useCallback((rule: RuleMigration) => { + setRuleMigrationForPreview(rule); + }, []), + closeRulePreview, + }; +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx new file mode 100644 index 0000000000000..3b13b9e631ccb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { SeverityBadge } from '../../../common/components/severity_badge'; +import * as rulesI18n from '../../../detections/pages/detection_engine/rules/translations'; +import * as i18n from './translations'; +import { getNormalizedSeverity } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers'; +import { StatusBadge } from '../components/status_badge'; +import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from '../utils/constants'; + +export type TableColumn = EuiBasicTableColumn<RuleMigration>; + +interface RuleNameProps { + name: string; + rule: RuleMigration; + openRulePreview: (rule: RuleMigration) => void; +} + +const RuleName = ({ name, rule, openRulePreview }: RuleNameProps) => { + return ( + <EuiLink + onClick={() => { + openRulePreview(rule); + }} + data-test-subj="ruleName" + > + {name} + </EuiLink> + ); +}; + +const createRuleNameColumn = ({ + openRulePreview, +}: { + openRulePreview: (rule: RuleMigration) => void; +}): TableColumn => { + return { + field: 'original_rule.title', + name: rulesI18n.COLUMN_RULE, + render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => ( + <RuleName name={value} rule={rule} openRulePreview={openRulePreview} /> + ), + sortable: true, + truncateText: true, + width: '40%', + align: 'left', + }; +}; + +const STATUS_COLUMN: TableColumn = { + field: 'translation_result', + name: i18n.COLUMN_STATUS, + render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( + <StatusBadge value={value} installedRuleId={rule.elastic_rule?.id} /> + ), + sortable: false, + truncateText: true, + width: '12%', +}; + +export const useRulesTableColumns = ({ + openRulePreview, +}: { + openRulePreview: (rule: RuleMigration) => void; +}): TableColumn[] => { + return useMemo( + () => [ + createRuleNameColumn({ openRulePreview }), + STATUS_COLUMN, + { + field: 'risk_score', + name: rulesI18n.COLUMN_RISK_SCORE, + render: () => ( + <EuiText data-test-subj="riskScore" size="s"> + {DEFAULT_TRANSLATION_RISK_SCORE} + </EuiText> + ), + sortable: true, + truncateText: true, + width: '75px', + }, + { + field: 'elastic_rule.severity', + name: rulesI18n.COLUMN_SEVERITY, + render: (value?: Severity) => ( + <SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} /> + ), + sortable: ({ elastic_rule: elasticRule }: RuleMigration) => + getNormalizedSeverity( + (elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY + ), + truncateText: true, + width: '12%', + }, + ], + [openRulePreview] + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx new file mode 100644 index 0000000000000..26199616b3777 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo } from 'react'; + +import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui'; +import type { RouteComponentProps } from 'react-router-dom'; +import { useNavigation } from '../../../common/lib/kibana'; +import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { HeaderPage } from '../../../common/components/header_page'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; +import { SecurityPageName } from '../../../app/types'; + +import * as i18n from './translations'; +import { RulesTable } from '../components/rules_table'; +import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout'; +import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; +import { HeaderButtons } from '../components/header_buttons'; +import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout'; +import { UnknownMigration } from '../components/unknown_migration'; +import { useLatestStats } from '../hooks/use_latest_stats'; + +type RulesMigrationPageProps = RouteComponentProps<{ migrationId?: string }>; + +export const RulesPage: React.FC<RulesMigrationPageProps> = React.memo( + ({ + match: { + params: { migrationId }, + }, + }) => { + const { navigateTo } = useNavigation(); + + const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats(); + + const migrationsIds = useMemo(() => { + if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) { + return []; + } + return ruleMigrationsStatsAll + .filter((migration) => migration.status === 'finished') + .map((migration) => migration.id); + }, [isLoadingMigrationsStats, ruleMigrationsStatsAll]); + + useEffect(() => { + if (isLoadingMigrationsStats) { + return; + } + + // Navigate to landing page if there are no migrations + if (!migrationsIds.length) { + navigateTo({ deepLinkId: SecurityPageName.landing, path: 'siem_migrations' }); + return; + } + + // Navigate to the most recent migration if none is selected + if (!migrationId) { + navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: migrationsIds[0] }); + } + }, [isLoadingMigrationsStats, migrationId, migrationsIds, navigateTo]); + + const onMigrationIdChange = (selectedId?: string) => { + navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId }); + }; + + const ruleActionsFactory = useCallback( + (ruleMigration: RuleMigration, closeRulePreview: () => void) => { + // TODO: Add flyout action buttons + return null; + }, + [] + ); + + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + ruleActionsFactory, + }); + + const content = useMemo(() => { + if (!migrationId || !migrationsIds.includes(migrationId)) { + return <UnknownMigration />; + } + return <RulesTable migrationId={migrationId} openRulePreview={openRulePreview} />; + }, [migrationId, migrationsIds, openRulePreview]); + + return ( + <> + <NeedAdminForUpdateRulesCallOut /> + <MissingPrivilegesCallOut /> + + <SecuritySolutionPageWrapper> + <HeaderPage title={i18n.PAGE_TITLE}> + <HeaderButtons + migrationsIds={migrationsIds} + selectedMigrationId={migrationId} + onMigrationIdChange={onMigrationIdChange} + /> + </HeaderPage> + <EuiSkeletonLoading + isLoading={isLoadingMigrationsStats} + loadingContent={ + <> + <EuiSkeletonTitle /> + <EuiSkeletonText /> + </> + } + loadedContent={content} + /> + {rulePreviewFlyout} + </SecuritySolutionPageWrapper> + </> + ); + } +); +RulesPage.displayName = 'RulesPage'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx new file mode 100644 index 0000000000000..3c95eaab8fe90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/translations.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.siemMigrations.rules.pageTitle', { + defaultMessage: 'Translated rules', +}); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts new file mode 100644 index 0000000000000..ba6543f5171d3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject, type Observable } from 'rxjs'; +import type { CoreStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; +import { licenseService } from '../../../common/hooks/use_license'; +import { getRuleMigrationsStatsAll } from '../api/api'; +import type { RuleMigrationStats } from '../types'; +import { getSuccessToast } from './success_notification'; + +const POLLING_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', + { defaultMessage: 'Error fetching rule migrations' } +); + +export class SiemRulesMigrationsService { + private readonly pollingInterval = 5000; + private readonly latestStats$: BehaviorSubject<RuleMigrationStats[]>; + private isPolling = false; + + constructor(private readonly core: CoreStart) { + this.latestStats$ = new BehaviorSubject<RuleMigrationStats[]>([]); + this.startPolling(); + } + + public getLatestStats$(): Observable<RuleMigrationStats[]> { + return this.latestStats$.asObservable(); + } + + public isAvailable() { + return ExperimentalFeaturesService.get().siemMigrationsEnabled && licenseService.isEnterprise(); + } + + public startPolling() { + if (this.isPolling || !this.isAvailable()) { + return; + } + + this.isPolling = true; + this.startStatsPolling() + .catch((e) => { + this.core.notifications.toasts.addError(e, { title: POLLING_ERROR_TITLE }); + }) + .finally(() => { + this.isPolling = false; + }); + } + + private async startStatsPolling(): Promise<void> { + let pendingMigrationIds: string[] = []; + do { + const results = await this.fetchRuleMigrationsStats(); + this.latestStats$.next(results); + + if (pendingMigrationIds.length > 0) { + // send notifications for finished migrations + pendingMigrationIds.forEach((pendingMigrationId) => { + const migration = results.find((item) => item.id === pendingMigrationId); + if (migration && migration.status === 'finished') { + this.core.notifications.toasts.addSuccess(getSuccessToast(migration, this.core)); + } + }); + } + + // reassign pending migrations + pendingMigrationIds = results.reduce<string[]>((acc, item) => { + if (item.status === 'running') { + acc.push(item.id); + } + return acc; + }, []); + + await new Promise((resolve) => setTimeout(resolve, this.pollingInterval)); + } while (pendingMigrationIds.length > 0); + } + + private async fetchRuleMigrationsStats(): Promise<RuleMigrationStats[]> { + const stats = await getRuleMigrationsStatsAll({ signal: new AbortController().signal }); + return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API + } +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx new file mode 100644 index 0000000000000..f87755943f830 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { i18n } from '@kbn/i18n'; +import { + SecurityPageName, + useNavigation, + NavigationProvider, +} from '@kbn/security-solution-navigation'; +import type { ToastInput } from '@kbn/core-notifications-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { RuleMigrationStats } from '../types'; + +export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ToastInput => ({ + color: 'success', + iconType: 'check', + toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes + title: i18n.translate('xpack.securitySolution.siemMigrations.rulesService.polling.successTitle', { + defaultMessage: 'Rules translation complete.', + }), + text: toMountPoint( + <NavigationProvider core={core}> + <SuccessToastContent migration={migration} /> + </NavigationProvider>, + core + ), +}); + +const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migration }) => { + const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id }; + + const { navigateTo, getAppUrl } = useNavigation(); + const onClick: React.MouseEventHandler = (ev) => { + ev.preventDefault(); + navigateTo(navigation); + }; + const url = getAppUrl(navigation); + + return ( + <EuiFlexGroup direction="column" alignItems="flexEnd" gutterSize="s"> + <EuiFlexItem> + <FormattedMessage + id="xpack.securitySolution.siemMigrations.rulesService.polling.successText" + defaultMessage="SIEM rules migration #{number} has finished translating. Results have been added to a dedicated page." + values={{ number: migration.number }} + /> + </EuiFlexItem> + <EuiFlexItem> + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + <EuiButton onClick={onClick} href={url} color="success"> + {i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.polling.successLinkText', + { defaultMessage: 'Go to translated rules' } + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts new file mode 100644 index 0000000000000..db9ca9507702f --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen'; + +export interface RuleMigrationStats extends RuleMigrationTaskStats { + /** The sequential number of the migration */ + number: number; +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts new file mode 100644 index 0000000000000..7400d4b0bcb63 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; + +export const DEFAULT_TRANSLATION_RISK_SCORE = 21; +export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx new file mode 100644 index 0000000000000..cd49311db21eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/helpers.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + RuleMigrationTranslationResultEnum, + type RuleMigrationTranslationResult, +} from '../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; + +export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => { + switch (status) { + case RuleMigrationTranslationResultEnum.full: + return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL; + + case RuleMigrationTranslationResultEnum.partial: + return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL; + + case RuleMigrationTranslationResultEnum.untranslatable: + return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL; + + default: + return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL; + } +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts new file mode 100644 index 0000000000000..bc098936c00f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.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 { i18n } from '@kbn/i18n'; + +export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.full', + { + defaultMessage: 'Fully translated', + } +); + +export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.partially', + { + defaultMessage: 'Partially translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable', + { + defaultMessage: 'Not translated', + } +); + +export const SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationResult.unknown', + { + defaultMessage: 'Unknown', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts new file mode 100644 index 0000000000000..08a50d018976b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/service/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; + +export type { SiemMigrationsService } from './siem_migrations_service'; + +export const createSiemMigrationsService = async (coreStart: CoreStart) => { + const { SiemMigrationsService } = await import( + /* webpackChunkName: "lazySiemMigrationsService" */ + './siem_migrations_service' + ); + return new SiemMigrationsService(coreStart); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts new file mode 100644 index 0000000000000..1775296f6e230 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/service/siem_migrations_service.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart } from '@kbn/core/public'; +import { SiemRulesMigrationsService } from '../rules/service/rule_migrations_service'; + +export class SiemMigrationsService { + public rules: SiemRulesMigrationsService; + + constructor(coreStart: CoreStart) { + this.rules = new SiemRulesMigrationsService(coreStart); + } +} diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 6642e02d5ecd6..d0387c5d3abe0 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -83,6 +83,7 @@ import type { EntityAnalytics } from './entity_analytics'; import type { Assets } from './assets'; import type { Investigations } from './investigations'; import type { MachineLearning } from './machine_learning'; +import type { SiemMigrations } from './siem_migrations'; import type { Dashboards } from './dashboards'; import type { BreadcrumbsNav } from './common/breadcrumbs/types'; @@ -93,6 +94,7 @@ import type { ConfigSettings } from '../common/config_settings'; import type { OnboardingService } from './onboarding/service'; import type { SolutionNavigation } from './app/solution_navigation/solution_navigation'; import type { TelemetryServiceStart } from './common/lib/telemetry'; +import type { SiemMigrationsService } from './siem_migrations/service'; export interface SetupPlugins { cloud?: CloudSetup; @@ -192,6 +194,7 @@ export type StartServices = CoreStart & customDataService: DataPublicPluginStart; topValuesPopover: TopValuesPopoverService; timelineDataService: DataPublicPluginStart; + siemMigrations: SiemMigrationsService; }; export type StartRenderServices = Pick< @@ -243,6 +246,7 @@ export interface SubPlugins { assets: Assets; investigations: Investigations; machineLearning: MachineLearning; + siemMigrations: SiemMigrations; } // TODO: find a better way to defined these types @@ -266,4 +270,5 @@ export interface StartedSubPlugins { assets: ReturnType<Assets['start']>; investigations: ReturnType<Investigations['start']>; machineLearning: ReturnType<MachineLearning['start']>; + siemMigrations: ReturnType<SiemMigrations['start']>; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts index 242639818566e..53c49d315ffac 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -9,4 +9,5 @@ export * from './artifacts'; export * from './actions'; export * from './agent'; export * from './artifacts_exception_list'; +export * from './workflow_insights'; export type { FeatureKeys } from './feature_usage'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/constants.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/constants.ts new file mode 100644 index 0000000000000..f0884f2214cb8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/constants.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 DATA_STREAM_PREFIX = '.security-workflow-insights'; +export const COMPONENT_TEMPLATE_NAME = `${DATA_STREAM_PREFIX}-component-template`; +export const INDEX_TEMPLATE_NAME = `${DATA_STREAM_PREFIX}-index-template`; +export const INGEST_PIPELINE_NAME = `${DATA_STREAM_PREFIX}-ingest-pipeline`; +export const DATA_STREAM_NAME = `${DATA_STREAM_PREFIX}-default`; + +export const TOTAL_FIELDS_LIMIT = 2000; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/errors.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/errors.ts new file mode 100644 index 0000000000000..fa2cba3a6cd75 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/errors.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EndpointError } from '../../../../common/endpoint/errors'; + +export class SecurityWorkflowInsightsFailedInitialized extends EndpointError { + constructor(msg: string) { + super(`security workflow insights service failed to initialize with error: ${msg}`); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/field_map_configurations.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/field_map_configurations.ts new file mode 100644 index 0000000000000..32cc845ab00d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/field_map_configurations.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { FieldMap } from '@kbn/data-stream-adapter'; + +export const securityWorkflowInsightsFieldMap: FieldMap = { + '@timestamp': { + type: 'date', + array: false, + required: true, + }, + message: { + type: 'text', + array: false, + required: true, + }, + // endpoint or other part of security + category: { + type: 'keyword', + array: false, + required: true, + }, + // incompatible_virus, noisy_process_tree, etc + type: { + type: 'keyword', + array: false, + required: true, + }, + // the creator of the insight + source: { + type: 'nested', + array: false, + required: true, + }, + // kibana-insight-task, llm-request-id, etc + 'source.id': { + type: 'keyword', + array: false, + required: true, + }, + // endpoint, kibana, llm, etc + 'source.type': { + type: 'keyword', + array: false, + required: true, + }, + // starting timestamp of the source data used to generate the insight + 'source.data_range_start': { + type: 'date', + array: false, + required: true, + }, + // ending timestamp of the source data used to generate the insight + 'source.data_range_end': { + type: 'date', + array: false, + required: true, + }, + // the target that this insight is created for + target: { + type: 'nested', + array: false, + required: true, + }, + // endpoint, policy, etc + 'target.id': { + type: 'keyword', + array: true, + required: true, + }, + // endpoint ids, policy ids, etc + 'target.type': { + type: 'keyword', + array: false, + required: true, + }, + // latest action taken on the insight + action: { + type: 'nested', + array: false, + required: true, + }, + // refreshed, remediated, suppressed, dismissed + 'action.type': { + type: 'keyword', + array: false, + required: true, + }, + 'action.timestamp': { + type: 'date', + array: false, + required: true, + }, + // unique key for this insight, used for deduplicating insights. + // ie. crowdstrike or windows_defender + value: { + type: 'keyword', + array: false, + required: true, + }, + // suggested remediation for insight + remediation: { + type: 'object', + array: false, + required: true, + }, + // if remediation includes exception list items + 'remediation.exception_list_items': { + type: 'object', + array: true, + required: false, + }, + 'remediation.exception_list_items.list_id': { + type: 'keyword', + array: false, + required: true, + }, + 'remediation.exception_list_items.name': { + type: 'text', + array: false, + required: true, + }, + 'remediation.exception_list_items.description': { + type: 'text', + array: false, + required: false, + }, + 'remediation.exception_list_items.entries': { + type: 'object', + array: true, + required: true, + }, + 'remediation.exception_list_items.entries.field': { + type: 'keyword', + array: false, + required: true, + }, + 'remediation.exception_list_items.entries.operator': { + type: 'keyword', + array: false, + required: true, + }, + 'remediation.exception_list_items.entries.type': { + type: 'keyword', + array: false, + required: true, + }, + 'remediation.exception_list_items.entries.value': { + type: 'text', + array: false, + required: true, + }, + 'remediation.exception_list_items.tags': { + type: 'keyword', + array: true, + required: true, + }, + 'remediation.exception_list_items.os_types': { + type: 'keyword', + array: true, + required: true, + }, + metadata: { + type: 'object', + array: false, + required: true, + }, + // optional KV for notes + 'metadata.notes': { + type: 'object', + array: false, + required: false, + }, + // optional i8n variables + 'metadata.message_variables': { + type: 'text', + array: true, + required: false, + }, +} as const; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts new file mode 100644 index 0000000000000..33f1851091167 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from '@kbn/core/server'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; +import { kibanaPackageJson } from '@kbn/repo-info'; + +import { createDatastream, createPipeline } from './helpers'; +import { + DATA_STREAM_PREFIX, + COMPONENT_TEMPLATE_NAME, + INDEX_TEMPLATE_NAME, + INGEST_PIPELINE_NAME, + TOTAL_FIELDS_LIMIT, +} from './constants'; +import { securityWorkflowInsightsFieldMap } from './field_map_configurations'; + +jest.mock('@kbn/data-stream-adapter', () => ({ + DataStreamSpacesAdapter: jest.fn().mockImplementation(() => ({ + setComponentTemplate: jest.fn(), + setIndexTemplate: jest.fn(), + })), +})); + +describe('helpers', () => { + describe('createDatastream', () => { + it('should create a DataStreamSpacesAdapter with the correct configuration', () => { + const kibanaVersion = kibanaPackageJson.version; + const ds = createDatastream(kibanaVersion); + + expect(DataStreamSpacesAdapter).toHaveBeenCalledTimes(1); + expect(DataStreamSpacesAdapter).toHaveBeenCalledWith(DATA_STREAM_PREFIX, { + kibanaVersion, + totalFieldsLimit: TOTAL_FIELDS_LIMIT, + }); + expect(ds.setComponentTemplate).toHaveBeenCalledTimes(1); + expect(ds.setComponentTemplate).toHaveBeenCalledWith({ + name: COMPONENT_TEMPLATE_NAME, + fieldMap: securityWorkflowInsightsFieldMap, + }); + expect(ds.setIndexTemplate).toHaveBeenCalledTimes(1); + expect(ds.setIndexTemplate).toHaveBeenCalledWith({ + name: INDEX_TEMPLATE_NAME, + componentTemplateRefs: [COMPONENT_TEMPLATE_NAME], + template: { + settings: { + default_pipeline: INGEST_PIPELINE_NAME, + }, + }, + hidden: true, + }); + }); + }); + + describe('createPipeline', () => { + let esClient: ElasticsearchClient; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createElasticsearchClient(); + }); + + it('should create an ingest pipeline with the correct configuration', async () => { + await createPipeline(esClient); + + expect(esClient.ingest.putPipeline).toHaveBeenCalledTimes(1); + expect(esClient.ingest.putPipeline).toHaveBeenCalledWith({ + id: INGEST_PIPELINE_NAME, + processors: [], + _meta: { + managed: true, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts new file mode 100644 index 0000000000000..54b449edf86ff --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from '@kbn/core/server'; + +import { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; + +import { + COMPONENT_TEMPLATE_NAME, + DATA_STREAM_PREFIX, + INDEX_TEMPLATE_NAME, + INGEST_PIPELINE_NAME, + TOTAL_FIELDS_LIMIT, +} from './constants'; +import { securityWorkflowInsightsFieldMap } from './field_map_configurations'; + +export function createDatastream(kibanaVersion: string): DataStreamSpacesAdapter { + const ds = new DataStreamSpacesAdapter(DATA_STREAM_PREFIX, { + kibanaVersion, + totalFieldsLimit: TOTAL_FIELDS_LIMIT, + }); + ds.setComponentTemplate({ + name: COMPONENT_TEMPLATE_NAME, + fieldMap: securityWorkflowInsightsFieldMap, + }); + ds.setIndexTemplate({ + name: INDEX_TEMPLATE_NAME, + componentTemplateRefs: [COMPONENT_TEMPLATE_NAME], + template: { + settings: { + default_pipeline: INGEST_PIPELINE_NAME, + }, + }, + hidden: true, + }); + return ds; +} + +export async function createPipeline(esClient: ElasticsearchClient): Promise<boolean> { + const response = await esClient.ingest.putPipeline({ + id: INGEST_PIPELINE_NAME, + processors: [ + // requires @elastic/elasticsearch 8.16.0 + // { + // fingerprint: { + // fields: ['type', 'category', 'value', 'target.type', 'target.id'], + // target_field: '_id', + // method: 'SHA-256', + // if: 'ctx._id == null', + // }, + // }, + ], + _meta: { + managed: true, + }, + }); + return response.acknowledged; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts new file mode 100644 index 0000000000000..6271bd780dedd --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReplaySubject } from 'rxjs'; + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; +import { loggerMock } from '@kbn/logging-mocks'; +import { kibanaPackageJson } from '@kbn/repo-info'; + +import { createDatastream, createPipeline } from './helpers'; +import { securityWorkflowInsightsService } from '.'; +import { DATA_STREAM_NAME } from './constants'; + +jest.mock('./helpers', () => ({ + createDatastream: jest.fn(), + createPipeline: jest.fn(), +})); + +describe('SecurityWorkflowInsightsService', () => { + let logger: Logger; + let esClient: ElasticsearchClient; + + beforeEach(() => { + logger = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('setup', () => { + it('should set up the data stream', () => { + const createDatastreamMock = createDatastream as jest.Mock; + createDatastreamMock.mockReturnValueOnce( + new DataStreamSpacesAdapter(DATA_STREAM_NAME, { + kibanaVersion: kibanaPackageJson.version, + }) + ); + + securityWorkflowInsightsService.setup({ + kibanaVersion: kibanaPackageJson.version, + logger, + isFeatureEnabled: true, + }); + + expect(createDatastreamMock).toHaveBeenCalledTimes(1); + expect(createDatastreamMock).toHaveBeenCalledWith(kibanaPackageJson.version); + }); + + it('should log a warning if createDatastream throws an error', () => { + const createDatastreamMock = createDatastream as jest.Mock; + createDatastreamMock.mockImplementation(() => { + throw new Error('test error'); + }); + + securityWorkflowInsightsService.setup({ + kibanaVersion: kibanaPackageJson.version, + logger, + isFeatureEnabled: true, + }); + + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('test error')); + }); + }); + + describe('start', () => { + it('should start the service', async () => { + const createDatastreamMock = createDatastream as jest.Mock; + const ds = new DataStreamSpacesAdapter(DATA_STREAM_NAME, { + kibanaVersion: kibanaPackageJson.version, + }); + const dsInstallSpy = jest.spyOn(ds, 'install'); + dsInstallSpy.mockResolvedValueOnce(); + createDatastreamMock.mockReturnValueOnce(ds); + const createPipelineMock = createPipeline as jest.Mock; + createPipelineMock.mockResolvedValueOnce(true); + const createDataStreamMock = esClient.indices.createDataStream as jest.Mock; + + securityWorkflowInsightsService.setup({ + kibanaVersion: kibanaPackageJson.version, + logger, + isFeatureEnabled: true, + }); + expect(createDatastreamMock).toHaveBeenCalledTimes(1); + expect(createDatastreamMock).toHaveBeenCalledWith(kibanaPackageJson.version); + + await securityWorkflowInsightsService.start({ esClient }); + + expect(createPipelineMock).toHaveBeenCalledTimes(1); + expect(createPipelineMock).toHaveBeenCalledWith(esClient); + expect(dsInstallSpy).toHaveBeenCalledTimes(1); + expect(dsInstallSpy).toHaveBeenCalledWith({ + logger, + esClient, + pluginStop$: expect.any(ReplaySubject), + }); + expect(createDataStreamMock).toHaveBeenCalledTimes(1); + expect(createDataStreamMock).toHaveBeenCalledWith({ name: DATA_STREAM_NAME }); + }); + + it('should log a warning if createPipeline or ds.install throws an error', async () => { + securityWorkflowInsightsService.setup({ + kibanaVersion: kibanaPackageJson.version, + logger, + isFeatureEnabled: true, + }); + + const createPipelineMock = createPipeline as jest.Mock; + createPipelineMock.mockImplementationOnce(() => { + throw new Error('test error'); + }); + + await securityWorkflowInsightsService.start({ esClient }); + + expect(logger.warn).toHaveBeenCalledTimes(2); + expect(logger.warn).toHaveBeenNthCalledWith(1, expect.stringContaining('test error')); + }); + }); + + describe('create', () => { + it('should wait for initialization', async () => { + const isInitializedSpy = jest + .spyOn(securityWorkflowInsightsService, 'isInitialized', 'get') + .mockResolvedValueOnce([undefined, undefined]); + + await securityWorkflowInsightsService.create(); + + expect(isInitializedSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('update', () => { + it('should wait for initialization', async () => { + const isInitializedSpy = jest + .spyOn(securityWorkflowInsightsService, 'isInitialized', 'get') + .mockResolvedValueOnce([undefined, undefined]); + + await securityWorkflowInsightsService.update(); + + expect(isInitializedSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('fetch', () => { + it('should wait for initialization', async () => { + const isInitializedSpy = jest + .spyOn(securityWorkflowInsightsService, 'isInitialized', 'get') + .mockResolvedValueOnce([undefined, undefined]); + + await securityWorkflowInsightsService.fetch(); + + expect(isInitializedSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts new file mode 100644 index 0000000000000..005be1b0398e1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReplaySubject, firstValueFrom, combineLatest } from 'rxjs'; + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; + +import type { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; + +import { SecurityWorkflowInsightsFailedInitialized } from './errors'; +import { createDatastream, createPipeline } from './helpers'; +import { DATA_STREAM_NAME } from './constants'; + +interface SetupInterface { + kibanaVersion: string; + logger: Logger; + isFeatureEnabled: boolean; +} + +interface StartInterface { + esClient: ElasticsearchClient; +} + +class SecurityWorkflowInsightsService { + private setup$ = new ReplaySubject<void>(1); + private start$ = new ReplaySubject<void>(1); + private stop$ = new ReplaySubject<void>(1); + private ds: DataStreamSpacesAdapter | undefined; + // private _esClient: ElasticsearchClient | undefined; + private _logger: Logger | undefined; + private _isInitialized: Promise<[void, void]> = firstValueFrom( + combineLatest<[void, void]>([this.setup$, this.start$]) + ); + private isFeatureEnabled = false; + + public get isInitialized() { + return this._isInitialized; + } + + public setup({ kibanaVersion, logger, isFeatureEnabled }: SetupInterface) { + this.isFeatureEnabled = isFeatureEnabled; + if (!isFeatureEnabled) { + return; + } + + this._logger = logger; + + try { + this.ds = createDatastream(kibanaVersion); + } catch (err) { + this.logger.warn(new SecurityWorkflowInsightsFailedInitialized(err.message).message); + return; + } + + this.setup$.next(); + } + + public async start({ esClient }: StartInterface) { + if (!this.isFeatureEnabled) { + return; + } + + // this._esClient = esClient; + await firstValueFrom(this.setup$); + + try { + await createPipeline(esClient); + await this.ds?.install({ + logger: this.logger, + esClient, + pluginStop$: this.stop$, + }); + await esClient.indices.createDataStream({ name: DATA_STREAM_NAME }); + } catch (err) { + // ignore datastream already exists error + if (err?.body?.error?.type === 'resource_already_exists_exception') { + return; + } + + this.logger.warn(new SecurityWorkflowInsightsFailedInitialized(err.message).message); + return; + } + + this.start$.next(); + } + + public stop() { + this.setup$.next(); + this.setup$.complete(); + this.start$.next(); + this.start$.complete(); + this.stop$.next(); + this.stop$.complete(); + } + + public async create() { + await this.isInitialized; + } + + public async update() { + await this.isInitialized; + } + + public async fetch() { + await this.isInitialized; + } + + // to be used in create/update/fetch above + // private get esClient(): ElasticsearchClient { + // if (!this._esClient) { + // throw new SecurityWorkflowInsightsFailedInitialized('no elasticsearch client found'); + // } + + // return this._esClient; + // } + + private get logger(): Logger { + if (!this._logger) { + throw new SecurityWorkflowInsightsFailedInitialized('no logger found'); + } + + return this._logger; + } +} + +export const securityWorkflowInsightsService = new SecurityWorkflowInsightsService(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.test.ts new file mode 100644 index 0000000000000..f6035c9e87ca0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ThreeVersionsOf } from '../../../../../../../../common/api/detection_engine'; +import { + ThreeWayMergeOutcome, + MissingVersion, + ThreeWayDiffConflict, +} from '../../../../../../../../common/api/detection_engine'; +import { forceTargetVersionDiffAlgorithm } from './force_target_version_diff_algorithm'; + +describe('forceTargetVersionDiffAlgorithm', () => { + describe('when base version exists', () => { + it('returns a NON conflict diff', () => { + const mockVersions: ThreeVersionsOf<number> = { + base_version: 1, + current_version: 1, + target_version: 2, + }; + + const result = forceTargetVersionDiffAlgorithm(mockVersions); + + expect(result).toMatchObject({ + conflict: ThreeWayDiffConflict.NONE, + }); + }); + + it('return merge outcome TARGET', () => { + const mockVersions: ThreeVersionsOf<number> = { + base_version: 1, + current_version: 1, + target_version: 2, + }; + + const result = forceTargetVersionDiffAlgorithm(mockVersions); + + expect(result).toMatchObject({ + has_base_version: true, + merge_outcome: ThreeWayMergeOutcome.Target, + }); + }); + }); + + describe('when base version missing', () => { + it('returns a NON conflict diff', () => { + const mockVersions: ThreeVersionsOf<number> = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = forceTargetVersionDiffAlgorithm(mockVersions); + + expect(result).toMatchObject({ + conflict: ThreeWayDiffConflict.NONE, + }); + }); + + it('return merge outcome TARGET', () => { + const mockVersions: ThreeVersionsOf<number> = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = forceTargetVersionDiffAlgorithm(mockVersions); + + expect(result).toMatchObject({ + has_base_version: false, + merge_outcome: ThreeWayMergeOutcome.Target, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.ts new file mode 100644 index 0000000000000..ed19be0275aec --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/force_target_version_diff_algorithm.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + ThreeVersionsOf, + ThreeWayDiff, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + MissingVersion, + ThreeWayDiffConflict, + ThreeWayMergeOutcome, + determineDiffOutcome, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; + +/** + * Diff algorithm forcing target version. Useful for special fields like `version`. + */ +export const forceTargetVersionDiffAlgorithm = <TValue>( + versions: ThreeVersionsOf<TValue> +): ThreeWayDiff<TValue> => { + const { + base_version: baseVersion, + current_version: currentVersion, + target_version: targetVersion, + } = versions; + const hasBaseVersion = baseVersion !== MissingVersion; + const hasUpdate = targetVersion !== currentVersion; + + return { + has_base_version: hasBaseVersion, + base_version: hasBaseVersion ? baseVersion : undefined, + current_version: currentVersion, + target_version: targetVersion, + merged_version: targetVersion, + merge_outcome: ThreeWayMergeOutcome.Target, + + diff_outcome: determineDiffOutcome(baseVersion, currentVersion, targetVersion), + has_update: hasUpdate, + conflict: ThreeWayDiffConflict.NONE, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts index c8b55a49edc00..b0f192cf70e42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts @@ -15,3 +15,4 @@ export { kqlQueryDiffAlgorithm } from './kql_query_diff_algorithm'; export { eqlQueryDiffAlgorithm } from './eql_query_diff_algorithm'; export { esqlQueryDiffAlgorithm } from './esql_query_diff_algorithm'; export { ruleTypeDiffAlgorithm } from './rule_type_diff_algorithm'; +export { forceTargetVersionDiffAlgorithm } from './force_target_version_diff_algorithm'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index 5730af03789d1..78ea28137bbf5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -48,6 +48,7 @@ import { eqlQueryDiffAlgorithm, esqlQueryDiffAlgorithm, ruleTypeDiffAlgorithm, + forceTargetVersionDiffAlgorithm, } from './algorithms'; const BASE_TYPE_ERROR = `Base version can't be of different rule type`; @@ -179,7 +180,11 @@ const calculateCommonFieldsDiff = ( const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields> = { rule_id: simpleDiffAlgorithm, - version: numberDiffAlgorithm, + /** + * `version` shouldn't have a conflict. It always get target value automatically. + * Diff has informational purpose. + */ + version: forceTargetVersionDiffAlgorithm, name: singleLineStringDiffAlgorithm, tags: scalarArrayDiffAlgorithm, description: multiLineStringDiffAlgorithm, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index b99fa8935b692..a89469acf569b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -241,19 +241,19 @@ export class EntityStoreDataClient { ) { const setupStartTime = moment().utc().toISOString(); const { logger, namespace, appClient, dataViewsService } = this.options; - const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService); + try { + const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService); - const unitedDefinition = getUnitedEntityDefinition({ - indexPatterns, - entityType, - namespace, - fieldHistoryLength, - syncDelay: `${config.syncDelay.asSeconds()}s`, - frequency: `${config.frequency.asSeconds()}s`, - }); - const { entityManagerDefinition } = unitedDefinition; + const unitedDefinition = getUnitedEntityDefinition({ + indexPatterns, + entityType, + namespace, + fieldHistoryLength, + syncDelay: `${config.syncDelay.asSeconds()}s`, + frequency: `${config.frequency.asSeconds()}s`, + }); + const { entityManagerDefinition } = unitedDefinition; - try { // clean up any existing entity store await this.delete(entityType, taskManager, { deleteData: false, deleteEngine: false }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index a937560842f74..21a17bb7834a1 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -8,11 +8,11 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { v4 as uuidV4 } from 'uuid'; +import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; import { CreateRuleMigrationRequestBody, type CreateRuleMigrationResponse, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; -import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { CreateRuleMigrationInput } from '../data/rule_migrations_data_rules_client'; import { withLicense } from './util/with_license'; @@ -47,7 +47,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( migration_id: migrationId, original_rule: originalRule, })); - + await ruleMigrationsClient.data.integrations.create(); await ruleMigrationsClient.data.rules.create(ruleMigrations); return res.ok({ body: { migration_id: migrationId } }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts index 34e68d8a47369..d8dc1bb168a72 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts @@ -32,10 +32,15 @@ export const MockRuleMigrationsDataResourcesClient = jest .fn() .mockImplementation(() => mockRuleMigrationsDataResourcesClient); +export const mockRuleMigrationsDataIntegrationsClient = { + retrieveIntegrations: jest.fn().mockResolvedValue([]), +}; + // Rule migrations data client export const mockRuleMigrationsDataClient = { rules: mockRuleMigrationsDataRulesClient, resources: mockRuleMigrationsDataResourcesClient, + integrations: mockRuleMigrationsDataIntegrationsClient, }; export const MockRuleMigrationsDataClient = jest diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/integrations_temp.json b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/integrations_temp.json new file mode 100644 index 0000000000000..9c312bb38e3d6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/integrations_temp.json @@ -0,0 +1,6881 @@ +[ + { + "title": "Containerd", + "id": "containerd", + "description": "Collect metrics from containerd containers.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-containerd.memory-*", + "title": "Containerd memory metrics" + }, + { + "dataset": "blkio", + "index_pattern": "logs-containerd.blkio-*", + "title": "Containerd blkio metrics" + }, + { + "dataset": "cpu", + "index_pattern": "logs-containerd.cpu-*", + "title": "Containerd cpu metrics" + } + ], + "elser_embedding": "Containerd - Collect metrics from containerd containers. - Containerd memory metrics Containerd blkio metrics Containerd cpu metrics" + }, + { + "title": "Google Santa", + "id": "santa", + "description": "Collect logs from Google Santa with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-santa.log-*", + "title": "Google Santa log logs" + } + ], + "elser_embedding": "Google Santa - Collect logs from Google Santa with Elastic Agent. - Google Santa log logs" + }, + { + "title": "Keycloak", + "id": "keycloak", + "description": "Collect logs from Keycloak with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-keycloak.log-*", + "title": "Keycloak" + } + ], + "elser_embedding": "Keycloak - Collect logs from Keycloak with Elastic Agent. - Keycloak" + }, + { + "title": "Infoblox NIOS", + "id": "infoblox_nios", + "description": "Collect logs from Infoblox NIOS with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-infoblox_nios.log-*", + "title": "Infoblox NIOS logs" + } + ], + "elser_embedding": "Infoblox NIOS - Collect logs from Infoblox NIOS with Elastic Agent. - Infoblox NIOS logs" + }, + { + "title": "LastPass", + "id": "lastpass", + "description": "Collect logs from LastPass with Elastic Agent.", + "data_streams": [ + { + "dataset": "detailed_shared_folder", + "index_pattern": "logs-lastpass.detailed_shared_folder-*", + "title": "Collect Detailed Shared Folder logs from LastPass" + }, + { + "dataset": "user", + "index_pattern": "logs-lastpass.user-*", + "title": "Collect User logs from LastPass" + }, + { + "dataset": "event_report", + "index_pattern": "logs-lastpass.event_report-*", + "title": "Collect Event Report logs from LastPass" + } + ], + "elser_embedding": "LastPass - Collect logs from LastPass with Elastic Agent. - Collect Detailed Shared Folder logs from LastPass Collect User logs from LastPass Collect Event Report logs from LastPass" + }, + { + "title": "IBM MQ", + "id": "ibmmq", + "description": "Collect logs and metrics from IBM MQ with Elastic Agent.", + "data_streams": [ + { + "dataset": "qmgr", + "index_pattern": "logs-ibmmq.qmgr-*", + "title": "IBM MQ Queue Manager performance metrics" + }, + { + "dataset": "errorlog", + "index_pattern": "logs-ibmmq.errorlog-*", + "title": "IBM MQ Error logs" + } + ], + "elser_embedding": "IBM MQ - Collect logs and metrics from IBM MQ with Elastic Agent. - IBM MQ Queue Manager performance metrics IBM MQ Error logs" + }, + { + "title": "Jamf Protect", + "id": "jamf_protect", + "description": "Receives events from Jamf Protect with Elastic Agent.", + "data_streams": [ + { + "dataset": "web_traffic_events", + "index_pattern": "logs-jamf_protect.web_traffic_events-*", + "title": "Receives Web Traffic Events from Jamf Protect with Elastic Agent." + }, + { + "dataset": "telemetry_legacy", + "index_pattern": "logs-jamf_protect.telemetry_legacy-*", + "title": "Jamf Protect Telemetry (Legacy)." + }, + { + "dataset": "web_threat_events", + "index_pattern": "logs-jamf_protect.web_threat_events-*", + "title": "Receives Web Threat Events from Jamf Protect with Elastic Agent." + }, + { + "dataset": "telemetry", + "index_pattern": "logs-jamf_protect.telemetry-*", + "title": "Receives Telemetry from Jamf Protect with Elastic Agent." + }, + { + "dataset": "alerts", + "index_pattern": "logs-jamf_protect.alerts-*", + "title": "Receives Alerts from Jamf Protect with Elastic Agent." + } + ], + "elser_embedding": "Jamf Protect - Receives events from Jamf Protect with Elastic Agent. - Receives Web Traffic Events from Jamf Protect with Elastic Agent. Jamf Protect Telemetry (Legacy). Receives Web Threat Events from Jamf Protect with Elastic Agent. Receives Telemetry from Jamf Protect with Elastic Agent. Receives Alerts from Jamf Protect with Elastic Agent." + }, + { + "title": "Sysmon for Linux", + "id": "sysmon_linux", + "description": "Collect Sysmon Linux logs with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-sysmon_linux.log-*", + "title": "Sysmon for Linux logs" + } + ], + "elser_embedding": "Sysmon for Linux - Collect Sysmon Linux logs with Elastic Agent. - Sysmon for Linux logs" + }, + { + "title": "Trend Micro Deep Security", + "id": "trendmicro", + "description": "Collect logs from Trend Micro Deep Security with Elastic Agent.", + "data_streams": [ + { + "dataset": "deep_security", + "index_pattern": "logs-trendmicro.deep_security-*", + "title": "Collect logs from Trend Micro Deep Security" + } + ], + "elser_embedding": "Trend Micro Deep Security - Collect logs from Trend Micro Deep Security with Elastic Agent. - Collect logs from Trend Micro Deep Security" + }, + { + "title": "HAProxy", + "id": "haproxy", + "description": "Collect logs and metrics from HAProxy servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "info", + "index_pattern": "logs-haproxy.info-*", + "title": "HAProxy info metrics" + }, + { + "dataset": "stat", + "index_pattern": "logs-haproxy.stat-*", + "title": "HAProxy stat metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-haproxy.log-*", + "title": "HAProxy logs" + } + ], + "elser_embedding": "HAProxy - Collect logs and metrics from HAProxy servers with Elastic Agent. - HAProxy info metrics HAProxy stat metrics HAProxy logs" + }, + { + "title": "ESET Threat Intelligence", + "id": "ti_eset", + "description": "Ingest threat intelligence indicators from ESET Threat Intelligence with Elastic Agent.", + "data_streams": [ + { + "dataset": "cc", + "index_pattern": "logs-ti_eset.cc-*", + "title": "Botnet C&C" + }, + { + "dataset": "url", + "index_pattern": "logs-ti_eset.url-*", + "title": "URL" + }, + { + "dataset": "domains", + "index_pattern": "logs-ti_eset.domains-*", + "title": "Domain" + }, + { + "dataset": "files", + "index_pattern": "logs-ti_eset.files-*", + "title": "Malicious files" + }, + { + "dataset": "apt", + "index_pattern": "logs-ti_eset.apt-*", + "title": "APT" + }, + { + "dataset": "ip", + "index_pattern": "logs-ti_eset.ip-*", + "title": "IP" + }, + { + "dataset": "botnet", + "index_pattern": "logs-ti_eset.botnet-*", + "title": "Botnet" + } + ], + "elser_embedding": "ESET Threat Intelligence - Ingest threat intelligence indicators from ESET Threat Intelligence with Elastic Agent. - Botnet C&C URL Domain Malicious files APT IP Botnet" + }, + { + "title": "Lyve Cloud", + "id": "lyve_cloud", + "description": "Collect S3 API audit log from Lyve Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-lyve_cloud.audit-*", + "title": "Lyve Cloud logs" + } + ], + "elser_embedding": "Lyve Cloud - Collect S3 API audit log from Lyve Cloud with Elastic Agent. - Lyve Cloud logs" + }, + { + "title": "Tanium", + "id": "tanium", + "description": "This Elastic integration collects logs from Tanium with Elastic Agent.", + "data_streams": [ + { + "dataset": "discover", + "index_pattern": "logs-tanium.discover-*", + "title": "Collect Tanium Discover logs from Tanium" + }, + { + "dataset": "threat_response", + "index_pattern": "logs-tanium.threat_response-*", + "title": "Collect Threat Response logs from Tanium." + }, + { + "dataset": "client_status", + "index_pattern": "logs-tanium.client_status-*", + "title": "Client Status" + }, + { + "dataset": "reporting", + "index_pattern": "logs-tanium.reporting-*", + "title": "Reporting" + }, + { + "dataset": "action_history", + "index_pattern": "logs-tanium.action_history-*", + "title": "Collect Action History logs from Tanium." + }, + { + "dataset": "endpoint_config", + "index_pattern": "logs-tanium.endpoint_config-*", + "title": "Collect Endpoint Config logs from Tanium" + } + ], + "elser_embedding": "Tanium - This Elastic integration collects logs from Tanium with Elastic Agent. - Collect Tanium Discover logs from Tanium Collect Threat Response logs from Tanium. Client Status Reporting Collect Action History logs from Tanium. Collect Endpoint Config logs from Tanium" + }, + { + "title": "SonicWall Firewall", + "id": "sonicwall_firewall", + "description": "Integration for SonicWall firewall logs", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-sonicwall_firewall.log-*", + "title": "SonicWall Firewall logs" + } + ], + "elser_embedding": "SonicWall Firewall - Integration for SonicWall firewall logs - SonicWall Firewall logs" + }, + { + "title": "STAN", + "id": "stan", + "description": "Collect logs and metrics from STAN servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "stats", + "index_pattern": "logs-stan.stats-*", + "title": "Stan stats metrics" + }, + { + "dataset": "subscriptions", + "index_pattern": "logs-stan.subscriptions-*", + "title": "Stan subscriptions metrics" + }, + { + "dataset": "channels", + "index_pattern": "logs-stan.channels-*", + "title": "Stan channels metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-stan.log-*", + "title": "STAN logs" + } + ], + "elser_embedding": "STAN - Collect logs and metrics from STAN servers with Elastic Agent. - Stan stats metrics Stan subscriptions metrics Stan channels metrics STAN logs" + }, + { + "title": "Amazon Bedrock", + "id": "aws_bedrock", + "description": "Collect Amazon Bedrock model invocation logs and runtime metrics with Elastic Agent.", + "data_streams": [ + { + "dataset": "runtime", + "index_pattern": "logs-aws_bedrock.runtime-*", + "title": "Amazon Bedrock Runtime Metrics" + }, + { + "dataset": "invocation", + "index_pattern": "logs-aws_bedrock.invocation-*", + "title": "Amazon Bedrock model invocation logs" + } + ], + "elser_embedding": "Amazon Bedrock - Collect Amazon Bedrock model invocation logs and runtime metrics with Elastic Agent. - Amazon Bedrock Runtime Metrics Amazon Bedrock model invocation logs" + }, + { + "title": "Microsoft M365 Defender", + "id": "m365_defender", + "description": "Collect logs from Microsoft M365 Defender with Elastic Agent.", + "data_streams": [ + { + "dataset": "alert", + "index_pattern": "logs-m365_defender.alert-*", + "title": "Collect Alert logs from Microsoft 365 Defender" + }, + { + "dataset": "log", + "index_pattern": "logs-m365_defender.log-*", + "title": "M365 Defender Logs" + }, + { + "dataset": "incident", + "index_pattern": "logs-m365_defender.incident-*", + "title": "Collect Incident logs from Microsoft 365 Defender" + }, + { + "dataset": "event", + "index_pattern": "logs-m365_defender.event-*", + "title": "Collect Event logs from Microsoft 365 Defender." + } + ], + "elser_embedding": "Microsoft M365 Defender - Collect logs from Microsoft M365 Defender with Elastic Agent. - Collect Alert logs from Microsoft 365 Defender M365 Defender Logs Collect Incident logs from Microsoft 365 Defender Collect Event logs from Microsoft 365 Defender." + }, + { + "title": "NATS", + "id": "nats", + "description": "Collect logs and metrics from NATS servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "stats", + "index_pattern": "logs-nats.stats-*", + "title": "NATS stats metrics" + }, + { + "dataset": "route", + "index_pattern": "logs-nats.route-*", + "title": "NATS route metrics" + }, + { + "dataset": "connection", + "index_pattern": "logs-nats.connection-*", + "title": "NATS connection metrics" + }, + { + "dataset": "subscriptions", + "index_pattern": "logs-nats.subscriptions-*", + "title": "NATS subscriptions metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-nats.log-*", + "title": "NATS logs" + }, + { + "dataset": "routes", + "index_pattern": "logs-nats.routes-*", + "title": "NATS routes metrics" + }, + { + "dataset": "connections", + "index_pattern": "logs-nats.connections-*", + "title": "NATS connections metrics" + } + ], + "elser_embedding": "NATS - Collect logs and metrics from NATS servers with Elastic Agent. - NATS stats metrics NATS route metrics NATS connection metrics NATS subscriptions metrics NATS logs NATS routes metrics NATS connections metrics" + }, + { + "title": "GoFlow2 logs", + "id": "goflow2", + "description": "Collect logs from goflow2 with Elastic Agent.", + "data_streams": [ + { + "dataset": "sflow", + "index_pattern": "logs-goflow2.sflow-*", + "title": "Goflow2 sFlow" + } + ], + "elser_embedding": "GoFlow2 logs - Collect logs from goflow2 with Elastic Agent. - Goflow2 sFlow" + }, + { + "title": "Microsoft Defender for Cloud", + "id": "microsoft_defender_cloud", + "description": "Collect logs from Microsoft Defender for Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-microsoft_defender_cloud.event-*", + "title": "Collect Event(Alert and Recommendation) logs from Microsoft Defender for Cloud." + } + ], + "elser_embedding": "Microsoft Defender for Cloud - Collect logs from Microsoft Defender for Cloud with Elastic Agent. - Collect Event(Alert and Recommendation) logs from Microsoft Defender for Cloud." + }, + { + "title": "RabbitMQ Logs and Metrics", + "id": "rabbitmq", + "description": "Collect and parse logs from RabbitMQ servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "queue", + "index_pattern": "logs-rabbitmq.queue-*", + "title": "RabbitMQ queue metrics" + }, + { + "dataset": "exchange", + "index_pattern": "logs-rabbitmq.exchange-*", + "title": "RabbitMQ exchange metrics" + }, + { + "dataset": "connection", + "index_pattern": "logs-rabbitmq.connection-*", + "title": "RabbitMQ connection metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-rabbitmq.log-*", + "title": "RabbitMQ application logs" + }, + { + "dataset": "node", + "index_pattern": "logs-rabbitmq.node-*", + "title": "RabbitMQ node metrics" + } + ], + "elser_embedding": "RabbitMQ Logs and Metrics - Collect and parse logs from RabbitMQ servers with Elastic Agent. - RabbitMQ queue metrics RabbitMQ exchange metrics RabbitMQ connection metrics RabbitMQ application logs RabbitMQ node metrics" + }, + { + "title": "Apache Tomcat", + "id": "apache_tomcat", + "description": "Collect and parse logs and metrics from Apache Tomcat servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "catalina", + "index_pattern": "logs-apache_tomcat.catalina-*", + "title": "Apache Tomcat Catalina logs" + }, + { + "dataset": "memory", + "index_pattern": "logs-apache_tomcat.memory-*", + "title": "Apache Tomcat Memory metrics" + }, + { + "dataset": "access", + "index_pattern": "logs-apache_tomcat.access-*", + "title": "Apache Tomcat Access logs" + }, + { + "dataset": "cache", + "index_pattern": "logs-apache_tomcat.cache-*", + "title": "Apache Tomcat Cache metrics" + }, + { + "dataset": "request", + "index_pattern": "logs-apache_tomcat.request-*", + "title": "Apache Tomcat Request metrics" + }, + { + "dataset": "session", + "index_pattern": "logs-apache_tomcat.session-*", + "title": "Apache Tomcat Session metrics" + }, + { + "dataset": "localhost", + "index_pattern": "logs-apache_tomcat.localhost-*", + "title": "Apache Tomcat Localhost logs" + }, + { + "dataset": "connection_pool", + "index_pattern": "logs-apache_tomcat.connection_pool-*", + "title": "Apache Tomcat Connection Pool metrics" + }, + { + "dataset": "thread_pool", + "index_pattern": "logs-apache_tomcat.thread_pool-*", + "title": "Apache Tomcat Thread Pool metrics" + } + ], + "elser_embedding": "Apache Tomcat - Collect and parse logs and metrics from Apache Tomcat servers with Elastic Agent. - Apache Tomcat Catalina logs Apache Tomcat Memory metrics Apache Tomcat Access logs Apache Tomcat Cache metrics Apache Tomcat Request metrics Apache Tomcat Session metrics Apache Tomcat Localhost logs Apache Tomcat Connection Pool metrics Apache Tomcat Thread Pool metrics" + }, + { + "title": "CylanceProtect Logs", + "id": "cylance", + "description": "Collect logs from CylanceProtect devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "protect", + "index_pattern": "logs-cylance.protect-*", + "title": "CylanceProtect logs" + } + ], + "elser_embedding": "CylanceProtect Logs - Collect logs from CylanceProtect devices with Elastic Agent. - CylanceProtect logs" + }, + { + "title": "Rapid7 InsightVM", + "id": "rapid7_insightvm", + "description": "Collect logs from Rapid7 InsightVM with Elastic Agent.", + "data_streams": [ + { + "dataset": "vulnerability", + "index_pattern": "logs-rapid7_insightvm.vulnerability-*", + "title": "Collect Vulnerability logs from Rapid7 InsightVM" + }, + { + "dataset": "asset", + "index_pattern": "logs-rapid7_insightvm.asset-*", + "title": "Collect Asset logs from Rapid7 InsightVM" + } + ], + "elser_embedding": "Rapid7 InsightVM - Collect logs from Rapid7 InsightVM with Elastic Agent. - Collect Vulnerability logs from Rapid7 InsightVM Collect Asset logs from Rapid7 InsightVM" + }, + { + "title": "Symantec EDR Cloud", + "id": "symantec_edr_cloud", + "description": "Collect logs from Symantec EDR Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "incident", + "index_pattern": "logs-symantec_edr_cloud.incident-*", + "title": "Collect Incident logs from Symantec EDR Cloud" + } + ], + "elser_embedding": "Symantec EDR Cloud - Collect logs from Symantec EDR Cloud with Elastic Agent. - Collect Incident logs from Symantec EDR Cloud" + }, + { + "title": "Nginx Ingress Controller OpenTelemetry Logs", + "id": "nginx_ingress_controller_otel", + "description": "Collect Nginx Ingress Controller logs using the OpenTelemetry collector.", + "data_streams": [], + "elser_embedding": "Nginx Ingress Controller OpenTelemetry Logs - Collect Nginx Ingress Controller logs using the OpenTelemetry collector. - " + }, + { + "title": "OpenCTI", + "id": "ti_opencti", + "description": "Ingest threat intelligence indicators from OpenCTI with Elastic Agent.", + "data_streams": [ + { + "dataset": "indicator", + "index_pattern": "logs-ti_opencti.indicator-*", + "title": "Indicator" + } + ], + "elser_embedding": "OpenCTI - Ingest threat intelligence indicators from OpenCTI with Elastic Agent. - Indicator" + }, + { + "title": "Windows", + "id": "windows", + "description": "Collect logs and metrics from Windows OS and services with Elastic Agent.", + "data_streams": [ + { + "dataset": "applocker_packaged_app_deployment", + "index_pattern": "logs-windows.applocker_packaged_app_deployment-*", + "title": "Windows AppLocker/Packaged app-Deployment logs" + }, + { + "dataset": "applocker_msi_and_script", + "index_pattern": "logs-windows.applocker_msi_and_script-*", + "title": "Windows AppLocker/MSI and Script logs" + }, + { + "dataset": "powershell_operational", + "index_pattern": "logs-windows.powershell_operational-*", + "title": "Windows Powershell/Operational logs" + }, + { + "dataset": "perfmon", + "index_pattern": "logs-windows.perfmon-*", + "title": "Windows perfmon metrics" + }, + { + "dataset": "windows_defender", + "index_pattern": "logs-windows.windows_defender-*", + "title": "Windows Defender logs" + }, + { + "dataset": "sysmon_operational", + "index_pattern": "logs-windows.sysmon_operational-*", + "title": "Windows Sysmon/Operational events" + }, + { + "dataset": "service", + "index_pattern": "logs-windows.service-*", + "title": "Windows service metrics" + }, + { + "dataset": "forwarded", + "index_pattern": "logs-windows.forwarded-*", + "title": "Windows forwarded events" + }, + { + "dataset": "powershell", + "index_pattern": "logs-windows.powershell-*", + "title": "Windows Powershell logs" + }, + { + "dataset": "applocker_exe_and_dll", + "index_pattern": "logs-windows.applocker_exe_and_dll-*", + "title": "Windows AppLocker/EXE and DLL logs" + }, + { + "dataset": "applocker_packaged_app_execution", + "index_pattern": "logs-windows.applocker_packaged_app_execution-*", + "title": "Windows AppLocker/Packaged app-Execution logs" + } + ], + "elser_embedding": "Windows - Collect logs and metrics from Windows OS and services with Elastic Agent. - Windows AppLocker/Packaged app-Deployment logs Windows AppLocker/MSI and Script logs Windows Powershell/Operational logs Windows perfmon metrics Windows Defender logs Windows Sysmon/Operational events Windows service metrics Windows forwarded events Windows Powershell logs Windows AppLocker/EXE and DLL logs Windows AppLocker/Packaged app-Execution logs" + }, + { + "title": "CouchDB", + "id": "couchdb", + "description": "Collect metrics from CouchDB with Elastic Agent.", + "data_streams": [ + { + "dataset": "server", + "index_pattern": "logs-couchdb.server-*", + "title": "Server" + } + ], + "elser_embedding": "CouchDB - Collect metrics from CouchDB with Elastic Agent. - Server" + }, + { + "title": "Custom UDP Logs", + "id": "udp", + "description": "Collect raw UDP data from listening UDP port with Elastic Agent.", + "data_streams": [ + { + "dataset": "generic", + "index_pattern": "logs-udp.generic-*", + "title": "Custom UDP Logs" + } + ], + "elser_embedding": "Custom UDP Logs - Collect raw UDP data from listening UDP port with Elastic Agent. - Custom UDP Logs" + }, + { + "title": "Cassandra", + "id": "cassandra", + "description": "This Elastic integration collects logs and metrics from cassandra.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cassandra.log-*", + "title": "Cassandra System Logs" + }, + { + "dataset": "metrics", + "index_pattern": "logs-cassandra.metrics-*", + "title": "Metrics" + } + ], + "elser_embedding": "Cassandra - This Elastic integration collects logs and metrics from cassandra. - Cassandra System Logs Metrics" + }, + { + "title": "Gigamon", + "id": "gigamon", + "description": "Collect logs from Gigamon with Elastic Agent.", + "data_streams": [ + { + "dataset": "ami", + "index_pattern": "logs-gigamon.ami-*", + "title": "Gigamon Application Metadata Intelligence (AMI) Logs" + } + ], + "elser_embedding": "Gigamon - Collect logs from Gigamon with Elastic Agent. - Gigamon Application Metadata Intelligence (AMI) Logs" + }, + { + "title": "Hashicorp Vault", + "id": "hashicorp_vault", + "description": "Collect logs and metrics from Hashicorp Vault with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-hashicorp_vault.audit-*", + "title": "Hashicorp Vault Audit Logs" + }, + { + "dataset": "log", + "index_pattern": "logs-hashicorp_vault.log-*", + "title": "Hashicorp Vault Operational Logs" + }, + { + "dataset": "metrics", + "index_pattern": "logs-hashicorp_vault.metrics-*", + "title": "Hashicorp Vault Metrics" + } + ], + "elser_embedding": "Hashicorp Vault - Collect logs and metrics from Hashicorp Vault with Elastic Agent. - Hashicorp Vault Audit Logs Hashicorp Vault Operational Logs Hashicorp Vault Metrics" + }, + { + "title": "Okta", + "id": "okta", + "description": "Collect and parse event logs from Okta API with Elastic Agent.", + "data_streams": [ + { + "dataset": "system", + "index_pattern": "logs-okta.system-*", + "title": "Okta system logs" + } + ], + "elser_embedding": "Okta - Collect and parse event logs from Okta API with Elastic Agent. - Okta system logs" + }, + { + "title": "Recorded Future", + "id": "ti_recordedfuture", + "description": "Ingest threat intelligence indicators from Recorded Future risk lists with Elastic Agent.", + "data_streams": [ + { + "dataset": "threat", + "index_pattern": "logs-ti_recordedfuture.threat-*", + "title": "Recorded Future" + } + ], + "elser_embedding": "Recorded Future - Ingest threat intelligence indicators from Recorded Future risk lists with Elastic Agent. - Recorded Future" + }, + { + "title": "IIS", + "id": "iis", + "description": "Collect logs and metrics from Internet Information Services (IIS) servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-iis.access-*", + "title": "IIS access logs" + }, + { + "dataset": "webserver", + "index_pattern": "logs-iis.webserver-*", + "title": "IIS web server metrics" + }, + { + "dataset": "error", + "index_pattern": "logs-iis.error-*", + "title": "IIS error logs" + }, + { + "dataset": "website", + "index_pattern": "logs-iis.website-*", + "title": "IIS website metrics" + }, + { + "dataset": "application_pool", + "index_pattern": "logs-iis.application_pool-*", + "title": "IIS application_pool metrics" + } + ], + "elser_embedding": "IIS - Collect logs and metrics from Internet Information Services (IIS) servers with Elastic Agent. - IIS access logs IIS web server metrics IIS error logs IIS website metrics IIS application_pool metrics" + }, + { + "title": "Golang", + "id": "golang", + "description": "This Elastic integration collects metrics from Golang applications.", + "data_streams": [ + { + "dataset": "heap", + "index_pattern": "logs-golang.heap-*", + "title": "Golang Heap metrics" + }, + { + "dataset": "expvar", + "index_pattern": "logs-golang.expvar-*", + "title": "Golang expvar metrics" + } + ], + "elser_embedding": "Golang - This Elastic integration collects metrics from Golang applications. - Golang Heap metrics Golang expvar metrics" + }, + { + "title": "MongoDB", + "id": "mongodb", + "description": "Collect logs and metrics from MongoDB instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "replstatus", + "index_pattern": "logs-mongodb.replstatus-*", + "title": "MongoDB replstatus metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-mongodb.log-*", + "title": "mongodb log logs" + }, + { + "dataset": "metrics", + "index_pattern": "logs-mongodb.metrics-*", + "title": "MongoDB metrics" + }, + { + "dataset": "status", + "index_pattern": "logs-mongodb.status-*", + "title": "MongoDB status metrics" + }, + { + "dataset": "dbstats", + "index_pattern": "logs-mongodb.dbstats-*", + "title": "MongoDB dbstats metrics" + }, + { + "dataset": "collstats", + "index_pattern": "logs-mongodb.collstats-*", + "title": "MongoDB collstats metrics" + } + ], + "elser_embedding": "MongoDB - Collect logs and metrics from MongoDB instances with Elastic Agent. - MongoDB replstatus metrics mongodb log logs MongoDB metrics MongoDB status metrics MongoDB dbstats metrics MongoDB collstats metrics" + }, + { + "title": "Sublime Security", + "id": "sublime_security", + "description": "Collect logs from Sublime Security with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-sublime_security.audit-*", + "title": "Sublime Security Audit logs" + }, + { + "dataset": "email_message", + "index_pattern": "logs-sublime_security.email_message-*", + "title": "Sublime Security Email Message logs" + }, + { + "dataset": "message_event", + "index_pattern": "logs-sublime_security.message_event-*", + "title": "Sublime Security Message Event logs" + } + ], + "elser_embedding": "Sublime Security - Collect logs from Sublime Security with Elastic Agent. - Sublime Security Audit logs Sublime Security Email Message logs Sublime Security Message Event logs" + }, + { + "title": "Nginx", + "id": "nginx", + "description": "Collect logs and metrics from Nginx HTTP servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-nginx.access-*", + "title": "Nginx access logs" + }, + { + "dataset": "error", + "index_pattern": "logs-nginx.error-*", + "title": "Nginx error logs" + }, + { + "dataset": "stubstatus", + "index_pattern": "logs-nginx.stubstatus-*", + "title": "Nginx stubstatus metrics" + } + ], + "elser_embedding": "Nginx - Collect logs and metrics from Nginx HTTP servers with Elastic Agent. - Nginx access logs Nginx error logs Nginx stubstatus metrics" + }, + { + "title": "Apache Spark", + "id": "apache_spark", + "description": "Collect metrics from Apache Spark with Elastic Agent.", + "data_streams": [ + { + "dataset": "driver", + "index_pattern": "logs-apache_spark.driver-*", + "title": "Apache Spark driver metrics" + }, + { + "dataset": "application", + "index_pattern": "logs-apache_spark.application-*", + "title": "Apache Spark application metrics" + }, + { + "dataset": "node", + "index_pattern": "logs-apache_spark.node-*", + "title": "Apache Spark node metrics" + }, + { + "dataset": "executor", + "index_pattern": "logs-apache_spark.executor-*", + "title": "Apache Spark executor metrics" + } + ], + "elser_embedding": "Apache Spark - Collect metrics from Apache Spark with Elastic Agent. - Apache Spark driver metrics Apache Spark application metrics Apache Spark node metrics Apache Spark executor metrics" + }, + { + "title": "Rapid7 Threat Command", + "id": "ti_rapid7_threat_command", + "description": "Collect threat intelligence from Threat Command API with Elastic Agent.", + "data_streams": [ + { + "dataset": "ioc", + "index_pattern": "logs-ti_rapid7_threat_command.ioc-*", + "title": "Rapid7 Threat Command IOCs" + }, + { + "dataset": "vulnerability", + "index_pattern": "logs-ti_rapid7_threat_command.vulnerability-*", + "title": "Rapid7 Threat Command Vulnerability" + }, + { + "dataset": "alert", + "index_pattern": "logs-ti_rapid7_threat_command.alert-*", + "title": "Rapid7 Threat Command Alerts" + } + ], + "elser_embedding": "Rapid7 Threat Command - Collect threat intelligence from Threat Command API with Elastic Agent. - Rapid7 Threat Command IOCs Rapid7 Threat Command Vulnerability Rapid7 Threat Command Alerts" + }, + { + "title": "Fortinet FortiEDR Logs", + "id": "fortinet_fortiedr", + "description": "Collect logs from Fortinet FortiEDR instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_fortiedr.log-*", + "title": "Fortinet FortEDR Endpoint Detection and Response logs" + } + ], + "elser_embedding": "Fortinet FortiEDR Logs - Collect logs from Fortinet FortiEDR instances with Elastic Agent. - Fortinet FortEDR Endpoint Detection and Response logs" + }, + { + "title": "ThreatQuotient", + "id": "ti_threatq", + "description": "Ingest threat intelligence indicators from ThreatQuotient with Elastic Agent.", + "data_streams": [ + { + "dataset": "threat", + "index_pattern": "logs-ti_threatq.threat-*", + "title": "ThreatQ" + } + ], + "elser_embedding": "ThreatQuotient - Ingest threat intelligence indicators from ThreatQuotient with Elastic Agent. - ThreatQ" + }, + { + "title": "BBOT (Bighuge BLS OSINT Tool)", + "id": "bbot", + "description": "BBOT is a recursive internet scanner inspired by Spiderfoot, but designed to be faster, more reliable, and friendlier to pentesters, bug bounty hunters, and developers. ", + "data_streams": [ + { + "dataset": "asm_intel", + "index_pattern": "logs-bbot.asm_intel-*", + "title": "BBOT-Data-Ingest" + } + ], + "elser_embedding": "BBOT (Bighuge BLS OSINT Tool) - BBOT is a recursive internet scanner inspired by Spiderfoot, but designed to be faster, more reliable, and friendlier to pentesters, bug bounty hunters, and developers. - BBOT-Data-Ingest" + }, + { + "title": "Microsoft SQL Server", + "id": "microsoft_sqlserver", + "description": "Collect events from Microsoft SQL Server with Elastic Agent", + "data_streams": [ + { + "dataset": "performance", + "index_pattern": "logs-microsoft_sqlserver.performance-*", + "title": "Microsoft SQL Server performance metrics" + }, + { + "dataset": "audit", + "index_pattern": "logs-microsoft_sqlserver.audit-*", + "title": "SQL Server audit events" + }, + { + "dataset": "log", + "index_pattern": "logs-microsoft_sqlserver.log-*", + "title": "Microsoft SQL Server error logs" + }, + { + "dataset": "transaction_log", + "index_pattern": "logs-microsoft_sqlserver.transaction_log-*", + "title": "Microsoft SQL Server transaction_log metrics" + } + ], + "elser_embedding": "Microsoft SQL Server - Collect events from Microsoft SQL Server with Elastic Agent - Microsoft SQL Server performance metrics SQL Server audit events Microsoft SQL Server error logs Microsoft SQL Server transaction_log metrics" + }, + { + "title": "Claroty CTD", + "id": "claroty_ctd", + "description": "Collect logs from Claroty CTD using Elastic Agent.", + "data_streams": [ + { + "dataset": "baseline", + "index_pattern": "logs-claroty_ctd.baseline-*", + "title": "Baseline logs" + }, + { + "dataset": "event", + "index_pattern": "logs-claroty_ctd.event-*", + "title": "Event logs" + }, + { + "dataset": "asset", + "index_pattern": "logs-claroty_ctd.asset-*", + "title": "Asset logs" + } + ], + "elser_embedding": "Claroty CTD - Collect logs from Claroty CTD using Elastic Agent. - Baseline logs Event logs Asset logs" + }, + { + "title": "ZeroFox", + "id": "zerofox", + "description": "Collect logs from ZeroFox with Elastic Agent.", + "data_streams": [ + { + "dataset": "alerts", + "index_pattern": "logs-zerofox.alerts-*", + "title": "Alerts" + } + ], + "elser_embedding": "ZeroFox - Collect logs from ZeroFox with Elastic Agent. - Alerts" + }, + { + "title": "Darktrace", + "id": "darktrace", + "description": "Collect logs from Darktrace with Elastic Agent.", + "data_streams": [ + { + "dataset": "system_status_alert", + "index_pattern": "logs-darktrace.system_status_alert-*", + "title": "Collect System Status Alert logs from Darktrace" + }, + { + "dataset": "model_breach_alert", + "index_pattern": "logs-darktrace.model_breach_alert-*", + "title": "Collect Model Breach Alert logs from Darktrace" + }, + { + "dataset": "ai_analyst_alert", + "index_pattern": "logs-darktrace.ai_analyst_alert-*", + "title": "Collect AI Analyst Alert logs from Darktrace" + } + ], + "elser_embedding": "Darktrace - Collect logs from Darktrace with Elastic Agent. - Collect System Status Alert logs from Darktrace Collect Model Breach Alert logs from Darktrace Collect AI Analyst Alert logs from Darktrace" + }, + { + "title": "Cybersixgill", + "id": "ti_cybersixgill", + "description": "Ingest threat intelligence indicators from Cybersixgill with Elastic Agent.", + "data_streams": [ + { + "dataset": "threat", + "index_pattern": "logs-ti_cybersixgill.threat-*", + "title": "Cybersixgill Darkfeed Logs" + } + ], + "elser_embedding": "Cybersixgill - Ingest threat intelligence indicators from Cybersixgill with Elastic Agent. - Cybersixgill Darkfeed Logs" + }, + { + "title": "Trend Micro Vision One", + "id": "trend_micro_vision_one", + "description": "Collect logs from Trend Micro Vision One with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-trend_micro_vision_one.audit-*", + "title": "Collect Audit logs from Trend Micro Vision One." + }, + { + "dataset": "alert", + "index_pattern": "logs-trend_micro_vision_one.alert-*", + "title": "Collect Alert logs from Trend Micro Vision One." + }, + { + "dataset": "detection", + "index_pattern": "logs-trend_micro_vision_one.detection-*", + "title": "Collect Detection logs from Trend Micro Vision One." + } + ], + "elser_embedding": "Trend Micro Vision One - Collect logs from Trend Micro Vision One with Elastic Agent. - Collect Audit logs from Trend Micro Vision One. Collect Alert logs from Trend Micro Vision One. Collect Detection logs from Trend Micro Vision One." + }, + { + "title": "Traefik", + "id": "traefik", + "description": "Collect logs from Traefik servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-traefik.access-*", + "title": "Traefik access logs" + }, + { + "dataset": "health", + "index_pattern": "logs-traefik.health-*", + "title": "Traefik health metrics" + } + ], + "elser_embedding": "Traefik - Collect logs from Traefik servers with Elastic Agent. - Traefik access logs Traefik health metrics" + }, + { + "title": "F5 BIG-IP", + "id": "f5_bigip", + "description": "Collect logs from F5 BIG-IP with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-f5_bigip.log-*", + "title": "Collect logs from F5 BIG-IP" + } + ], + "elser_embedding": "F5 BIG-IP - Collect logs from F5 BIG-IP with Elastic Agent. - Collect logs from F5 BIG-IP" + }, + { + "title": "Custom Kafka Logs", + "id": "kafka_log", + "description": "Collect data from kafka topic with Elastic Agent.", + "data_streams": [ + { + "dataset": "generic", + "index_pattern": "logs-kafka_log.generic-*", + "title": "Custom Kafka Logs" + } + ], + "elser_embedding": "Custom Kafka Logs - Collect data from kafka topic with Elastic Agent. - Custom Kafka Logs" + }, + { + "title": "CyberArk Privileged Access Security", + "id": "cyberarkpas", + "description": "Collect logs from CyberArk Privileged Access Security with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-cyberarkpas.audit-*", + "title": "CyberArk PAS audit logs" + }, + { + "dataset": "monitor", + "index_pattern": "logs-cyberarkpas.monitor-*", + "title": "CyberArk PAS monitor Events" + } + ], + "elser_embedding": "CyberArk Privileged Access Security - Collect logs from CyberArk Privileged Access Security with Elastic Agent. - CyberArk PAS audit logs CyberArk PAS monitor Events" + }, + { + "title": "Palo Alto Prisma Cloud", + "id": "prisma_cloud", + "description": "Collect logs from Prisma Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "host_profile", + "index_pattern": "logs-prisma_cloud.host_profile-*", + "title": "Collect Host Profile logs from Prisma Cloud Workload Protection." + }, + { + "dataset": "host", + "index_pattern": "logs-prisma_cloud.host-*", + "title": "Collect Host logs from Prisma Cloud Workload Protection." + }, + { + "dataset": "audit", + "index_pattern": "logs-prisma_cloud.audit-*", + "title": "Collect Audit logs from Prisma Cloud Security Posture Management." + }, + { + "dataset": "alert", + "index_pattern": "logs-prisma_cloud.alert-*", + "title": "Collect Alert logs from Prisma Cloud Security Posture Management." + }, + { + "dataset": "incident_audit", + "index_pattern": "logs-prisma_cloud.incident_audit-*", + "title": "Collect Incident Audit logs from Prisma Cloud Workload Protection." + } + ], + "elser_embedding": "Palo Alto Prisma Cloud - Collect logs from Prisma Cloud with Elastic Agent. - Collect Host Profile logs from Prisma Cloud Workload Protection. Collect Host logs from Prisma Cloud Workload Protection. Collect Audit logs from Prisma Cloud Security Posture Management. Collect Alert logs from Prisma Cloud Security Posture Management. Collect Incident Audit logs from Prisma Cloud Workload Protection." + }, + { + "title": "Cilium Tetragon", + "id": "cilium_tetragon", + "description": "Collect Cilium Tetragon logs from Kubernetes environments.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cilium_tetragon.log-*", + "title": "log\n" + } + ], + "elser_embedding": "Cilium Tetragon - Collect Cilium Tetragon logs from Kubernetes environments. - log\n" + }, + { + "title": "Qualys VMDR", + "id": "qualys_vmdr", + "description": "Collect data from Qualys VMDR platform with Elastic Agent.", + "data_streams": [ + { + "dataset": "knowledge_base", + "index_pattern": "logs-qualys_vmdr.knowledge_base-*", + "title": "Collect Knowledge Base data from Qualys VMDR platform." + }, + { + "dataset": "user_activity", + "index_pattern": "logs-qualys_vmdr.user_activity-*", + "title": "Collect User Activity Log data from Qualys VMDR platform." + }, + { + "dataset": "asset_host_detection", + "index_pattern": "logs-qualys_vmdr.asset_host_detection-*", + "title": "Collect Asset Host Detection data from Qualys VMDR platform." + } + ], + "elser_embedding": "Qualys VMDR - Collect data from Qualys VMDR platform with Elastic Agent. - Collect Knowledge Base data from Qualys VMDR platform. Collect User Activity Log data from Qualys VMDR platform. Collect Asset Host Detection data from Qualys VMDR platform." + }, + { + "title": "Elastic Agent", + "id": "elastic_agent", + "description": "Collect logs and metrics from Elastic Agents.", + "data_streams": [ + { + "dataset": "fleet_server_logs", + "index_pattern": "logs-elastic_agent.fleet_server_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "endpoint_security_metrics", + "index_pattern": "logs-elastic_agent.endpoint_security_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "apm_server_logs", + "index_pattern": "logs-elastic_agent.apm_server_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "osquerybeat_logs", + "index_pattern": "logs-elastic_agent.osquerybeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "heartbeat_logs", + "index_pattern": "logs-elastic_agent.heartbeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "metricbeat_logs", + "index_pattern": "logs-elastic_agent.metricbeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "elastic_agent_metrics", + "index_pattern": "logs-elastic_agent.elastic_agent_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "auditbeat_metrics", + "index_pattern": "logs-elastic_agent.auditbeat_metrics-*", + "title": "Elastic Agent Auditbeat Metrics" + }, + { + "dataset": "pf_elastic_symbolizer", + "index_pattern": "logs-elastic_agent.pf_elastic_symbolizer-*", + "title": "Elastic Agent" + }, + { + "dataset": "cloud_defend_logs", + "index_pattern": "logs-elastic_agent.cloud_defend_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "endpoint_sercurity_logs", + "index_pattern": "logs-elastic_agent.endpoint_sercurity_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "filebeat_input_metrics", + "index_pattern": "logs-elastic_agent.filebeat_input_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "metricbeat_metrics", + "index_pattern": "logs-elastic_agent.metricbeat_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "packetbeat_metrics", + "index_pattern": "logs-elastic_agent.packetbeat_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "apm_server_metrics", + "index_pattern": "logs-elastic_agent.apm_server_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "filebeat_input_logs", + "index_pattern": "logs-elastic_agent.filebeat_input_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "elastic_agent_logs", + "index_pattern": "logs-elastic_agent.elastic_agent_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "auditbeat_logs", + "index_pattern": "logs-elastic_agent.auditbeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "filebeat_logs", + "index_pattern": "logs-elastic_agent.filebeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "pf_host_agent_logs", + "index_pattern": "logs-elastic_agent.pf_host_agent_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "cloudbeat_logs", + "index_pattern": "logs-elastic_agent.cloudbeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "heartbeat_metrics", + "index_pattern": "logs-elastic_agent.heartbeat_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "cloudbeat_metrics", + "index_pattern": "logs-elastic_agent.cloudbeat_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "fleet_server_metrics", + "index_pattern": "logs-elastic_agent.fleet_server_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "packetbeat_logs", + "index_pattern": "logs-elastic_agent.packetbeat_logs-*", + "title": "Elastic Agent" + }, + { + "dataset": "osquerybeat_metrics", + "index_pattern": "logs-elastic_agent.osquerybeat_metrics-*", + "title": "Elastic Agent" + }, + { + "dataset": "pf_elastic_collector", + "index_pattern": "logs-elastic_agent.pf_elastic_collector-*", + "title": "Elastic Agent" + }, + { + "dataset": "filebeat_metrics", + "index_pattern": "logs-elastic_agent.filebeat_metrics-*", + "title": "Elastic Agent" + } + ], + "elser_embedding": "Elastic Agent - Collect logs and metrics from Elastic Agents. - Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Auditbeat Metrics Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent Elastic Agent" + }, + { + "title": "Custom Filestream Logs", + "id": "filestream", + "description": "Collect log data using filestream with Elastic Agent.", + "data_streams": [ + { + "dataset": "generic", + "index_pattern": "logs-filestream.generic-*", + "title": "Custom Filestream Logs" + } + ], + "elser_embedding": "Custom Filestream Logs - Collect log data using filestream with Elastic Agent. - Custom Filestream Logs" + }, + { + "title": "OpenCanary", + "id": "opencanary", + "description": "This integration collects and parses logs from OpenCanary honeypots.", + "data_streams": [ + { + "dataset": "events", + "index_pattern": "logs-opencanary.events-*", + "title": "OpenCanary HoneyPot Events" + } + ], + "elser_embedding": "OpenCanary - This integration collects and parses logs from OpenCanary honeypots. - OpenCanary HoneyPot Events" + }, + { + "title": "Palo Alto Cortex XDR", + "id": "panw_cortex_xdr", + "description": "Collect logs from Palo Alto Cortex XDR with Elastic Agent.", + "data_streams": [ + { + "dataset": "incidents", + "index_pattern": "logs-panw_cortex_xdr.incidents-*", + "title": "Palo Alto Cortex XDR Incidents API" + }, + { + "dataset": "alerts", + "index_pattern": "logs-panw_cortex_xdr.alerts-*", + "title": "Palo Alto Cortex XDR Alerts API" + } + ], + "elser_embedding": "Palo Alto Cortex XDR - Collect logs from Palo Alto Cortex XDR with Elastic Agent. - Palo Alto Cortex XDR Incidents API Palo Alto Cortex XDR Alerts API" + }, + { + "title": "Cisco Nexus", + "id": "cisco_nexus", + "description": "Collect logs from Cisco Nexus with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_nexus.log-*", + "title": "Collect logs from Cisco Nexus" + } + ], + "elser_embedding": "Cisco Nexus - Collect logs from Cisco Nexus with Elastic Agent. - Collect logs from Cisco Nexus" + }, + { + "title": "JumpCloud", + "id": "jumpcloud", + "description": "Collect logs from JumpCloud Directory as a Service", + "data_streams": [ + { + "dataset": "events", + "index_pattern": "logs-jumpcloud.events-*", + "title": "JumpCloud Directory as a Service Events" + } + ], + "elser_embedding": "JumpCloud - Collect logs from JumpCloud Directory as a Service - JumpCloud Directory as a Service Events" + }, + { + "title": "Microsoft Defender for Endpoint", + "id": "microsoft_defender_endpoint", + "description": "Collect logs from Microsoft Defender for Endpoint with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-microsoft_defender_endpoint.log-*", + "title": "Microsoft Defender for Endpoint logs" + } + ], + "elser_embedding": "Microsoft Defender for Endpoint - Collect logs from Microsoft Defender for Endpoint with Elastic Agent. - Microsoft Defender for Endpoint logs" + }, + { + "title": "ActiveMQ", + "id": "activemq", + "description": "Collect logs and metrics from ActiveMQ instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "broker", + "index_pattern": "logs-activemq.broker-*", + "title": "ActiveMQ broker metrics" + }, + { + "dataset": "queue", + "index_pattern": "logs-activemq.queue-*", + "title": "ActiveMQ queue metrics" + }, + { + "dataset": "audit", + "index_pattern": "logs-activemq.audit-*", + "title": "ActiveMQ audit logs" + }, + { + "dataset": "log", + "index_pattern": "logs-activemq.log-*", + "title": "ActiveMQ log logs" + }, + { + "dataset": "topic", + "index_pattern": "logs-activemq.topic-*", + "title": "ActiveMQ topic metrics" + } + ], + "elser_embedding": "ActiveMQ - Collect logs and metrics from ActiveMQ instances with Elastic Agent. - ActiveMQ broker metrics ActiveMQ queue metrics ActiveMQ audit logs ActiveMQ log logs ActiveMQ topic metrics" + }, + { + "title": "AbuseCH", + "id": "ti_abusech", + "description": "Ingest threat intelligence indicators from URL Haus, Malware Bazaar, and Threat Fox feeds with Elastic Agent.", + "data_streams": [ + { + "dataset": "threatfox", + "index_pattern": "logs-ti_abusech.threatfox-*", + "title": "AbuseCH Threat Fox indicators" + }, + { + "dataset": "url", + "index_pattern": "logs-ti_abusech.url-*", + "title": "AbuseCH URL logs" + }, + { + "dataset": "malware", + "index_pattern": "logs-ti_abusech.malware-*", + "title": "AbuseCH Malware payloads" + }, + { + "dataset": "malwarebazaar", + "index_pattern": "logs-ti_abusech.malwarebazaar-*", + "title": "AbuseCH MalwareBazaar payloads" + } + ], + "elser_embedding": "AbuseCH - Ingest threat intelligence indicators from URL Haus, Malware Bazaar, and Threat Fox feeds with Elastic Agent. - AbuseCH Threat Fox indicators AbuseCH URL logs AbuseCH Malware payloads AbuseCH MalwareBazaar payloads" + }, + { + "title": "Infoblox BloxOne DDI", + "id": "infoblox_bloxone_ddi", + "description": "Collect logs from Infoblox BloxOne DDI with Elastic Agent.", + "data_streams": [ + { + "dataset": "dns_data", + "index_pattern": "logs-infoblox_bloxone_ddi.dns_data-*", + "title": "Collect DNS Data logs from Infoblox BloxOne DDI" + }, + { + "dataset": "dns_config", + "index_pattern": "logs-infoblox_bloxone_ddi.dns_config-*", + "title": "Collect DNS Config logs from Infoblox BloxOne DDI" + }, + { + "dataset": "dhcp_lease", + "index_pattern": "logs-infoblox_bloxone_ddi.dhcp_lease-*", + "title": "Collect DHCP Lease logs from Infoblox BloxOne DDI" + } + ], + "elser_embedding": "Infoblox BloxOne DDI - Collect logs from Infoblox BloxOne DDI with Elastic Agent. - Collect DNS Data logs from Infoblox BloxOne DDI Collect DNS Config logs from Infoblox BloxOne DDI Collect DHCP Lease logs from Infoblox BloxOne DDI" + }, + { + "title": "Google Security Command Center", + "id": "google_scc", + "description": "Collect logs from Google Security Command Center with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-google_scc.audit-*", + "title": "Collect Audit logs from Google Security Command Center." + }, + { + "dataset": "finding", + "index_pattern": "logs-google_scc.finding-*", + "title": "Collect Finding logs from Google Security Command Center." + }, + { + "dataset": "asset", + "index_pattern": "logs-google_scc.asset-*", + "title": "Collect Asset logs from Google Security Command Center." + }, + { + "dataset": "source", + "index_pattern": "logs-google_scc.source-*", + "title": "Collect Source logs from Google Security Command Center." + } + ], + "elser_embedding": "Google Security Command Center - Collect logs from Google Security Command Center with Elastic Agent. - Collect Audit logs from Google Security Command Center. Collect Finding logs from Google Security Command Center. Collect Asset logs from Google Security Command Center. Collect Source logs from Google Security Command Center." + }, + { + "title": "CoreDNS", + "id": "coredns", + "description": "Collect logs from CoreDNS instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-coredns.log-*", + "title": "CoreDNS logs" + } + ], + "elser_embedding": "CoreDNS - Collect logs from CoreDNS instances with Elastic Agent. - CoreDNS logs" + }, + { + "title": "NetFlow Records", + "id": "netflow", + "description": "Collect flow records from NetFlow and IPFIX exporters with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-netflow.log-*", + "title": "NetFlow logs" + } + ], + "elser_embedding": "NetFlow Records - Collect flow records from NetFlow and IPFIX exporters with Elastic Agent. - NetFlow logs" + }, + { + "title": "Forcepoint Web Security", + "id": "forcepoint_web", + "description": "Forcepoint Web Security", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-forcepoint_web.logs-*", + "title": "Forcepoint Web Security Logs" + } + ], + "elser_embedding": "Forcepoint Web Security - Forcepoint Web Security - Forcepoint Web Security Logs" + }, + { + "title": "Trellix EDR Cloud", + "id": "trellix_edr_cloud", + "description": "Collect logs from Trellix EDR Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-trellix_edr_cloud.event-*", + "title": "Collect Event logs from Trellix EDR Cloud." + } + ], + "elser_embedding": "Trellix EDR Cloud - Collect logs from Trellix EDR Cloud with Elastic Agent. - Collect Event logs from Trellix EDR Cloud." + }, + { + "title": "Slack Logs", + "id": "slack", + "description": "Slack Logs Integration", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-slack.audit-*", + "title": "Slack Audit Logs" + } + ], + "elser_embedding": "Slack Logs - Slack Logs Integration - Slack Audit Logs" + }, + { + "title": "Cisco FTD", + "id": "cisco_ftd", + "description": "Collect logs from Cisco FTD with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_ftd.log-*", + "title": "Cisco FTD logs" + } + ], + "elser_embedding": "Cisco FTD - Collect logs from Cisco FTD with Elastic Agent. - Cisco FTD logs" + }, + { + "title": "Microsoft DNS Server", + "id": "microsoft_dnsserver", + "description": "Collect logs from Microsoft DNS Server with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-microsoft_dnsserver.audit-*", + "title": "Collect DNS Server Audit logs" + }, + { + "dataset": "analytical", + "index_pattern": "logs-microsoft_dnsserver.analytical-*", + "title": "Collect DNS Server Analytical logs" + } + ], + "elser_embedding": "Microsoft DNS Server - Collect logs from Microsoft DNS Server with Elastic Agent. - Collect DNS Server Audit logs Collect DNS Server Analytical logs" + }, + { + "title": "Mandiant Advantage", + "id": "ti_mandiant_advantage", + "description": "Collect Threat Intelligence from products within the Mandiant Advantage platform.", + "data_streams": [ + { + "dataset": "threat_intelligence", + "index_pattern": "logs-ti_mandiant_advantage.threat_intelligence-*", + "title": "Mandiant Threat Intelligence" + } + ], + "elser_embedding": "Mandiant Advantage - Collect Threat Intelligence from products within the Mandiant Advantage platform. - Mandiant Threat Intelligence" + }, + { + "title": "Fortinet FortiClient Logs", + "id": "fortinet_forticlient", + "description": "Collect logs from Fortinet FortiClient instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_forticlient.log-*", + "title": "Fortinet FortiClient Endpoint Security logs" + } + ], + "elser_embedding": "Fortinet FortiClient Logs - Collect logs from Fortinet FortiClient instances with Elastic Agent. - Fortinet FortiClient Endpoint Security logs" + }, + { + "title": "AWS Fargate (for ECS clusters)", + "id": "awsfargate", + "description": "Collects metrics from containers and tasks running on Amazon ECS clusters with Elastic Agent.", + "data_streams": [ + { + "dataset": "task_stats", + "index_pattern": "logs-awsfargate.task_stats-*", + "title": "AWS Fargate task_stats metrics" + } + ], + "elser_embedding": "AWS Fargate (for ECS clusters) - Collects metrics from containers and tasks running on Amazon ECS clusters with Elastic Agent. - AWS Fargate task_stats metrics" + }, + { + "title": "Azure Network Watcher VNet", + "id": "azure_network_watcher_vnet", + "description": "Collect logs from Azure Network Watcher VNet with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-azure_network_watcher_vnet.log-*", + "title": "Collect VNet logs from Azure Network Watcher" + } + ], + "elser_embedding": "Azure Network Watcher VNet - Collect logs from Azure Network Watcher VNet with Elastic Agent. - Collect VNet logs from Azure Network Watcher" + }, + { + "title": "Osquery Logs", + "id": "osquery", + "description": "Collect logs from Osquery with Elastic Agent.", + "data_streams": [ + { + "dataset": "result", + "index_pattern": "logs-osquery.result-*", + "title": "Osquery result logs" + } + ], + "elser_embedding": "Osquery Logs - Collect logs from Osquery with Elastic Agent. - Osquery result logs" + }, + { + "title": "Pleasant Password Server", + "id": "pps", + "description": "Integration for Pleasant Password Server Syslog Messages", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-pps.log-*", + "title": "Pleasant Password Server logs" + } + ], + "elser_embedding": "Pleasant Password Server - Integration for Pleasant Password Server Syslog Messages - Pleasant Password Server logs" + }, + { + "title": "Bravura Monitor", + "id": "hid_bravura_monitor", + "description": "Collect logs from Bravura Security Fabric with Elastic Agent.", + "data_streams": [ + { + "dataset": "winlog", + "index_pattern": "logs-hid_bravura_monitor.winlog-*", + "title": "Bravura Security Fabric logs" + }, + { + "dataset": "log", + "index_pattern": "logs-hid_bravura_monitor.log-*", + "title": "Bravura Monitor" + } + ], + "elser_embedding": "Bravura Monitor - Collect logs from Bravura Security Fabric with Elastic Agent. - Bravura Security Fabric logs Bravura Monitor" + }, + { + "title": "MISP", + "id": "ti_misp", + "description": "Ingest threat intelligence indicators from MISP platform with Elastic Agent.", + "data_streams": [ + { + "dataset": "threat", + "index_pattern": "logs-ti_misp.threat-*", + "title": "MISP" + }, + { + "dataset": "threat_attributes", + "index_pattern": "logs-ti_misp.threat_attributes-*", + "title": "MISP" + } + ], + "elser_embedding": "MISP - Ingest threat intelligence indicators from MISP platform with Elastic Agent. - MISP MISP" + }, + { + "title": "Redis Enterprise", + "id": "redisenterprise", + "description": "Collect metrics from Redis Enterprise Cluster", + "data_streams": [ + { + "dataset": "node", + "index_pattern": "logs-redisenterprise.node-*", + "title": "node" + }, + { + "dataset": "proxy", + "index_pattern": "logs-redisenterprise.proxy-*", + "title": "proxy" + } + ], + "elser_embedding": "Redis Enterprise - Collect metrics from Redis Enterprise Cluster - node proxy" + }, + { + "title": "Network Packet Capture", + "id": "network_traffic", + "description": "Capture and analyze network traffic from a host with Elastic Agent.", + "data_streams": [ + { + "dataset": "nfs", + "index_pattern": "logs-network_traffic.nfs-*", + "title": "NFS" + }, + { + "dataset": "tls", + "index_pattern": "logs-network_traffic.tls-*", + "title": "TLS" + }, + { + "dataset": "icmp", + "index_pattern": "logs-network_traffic.icmp-*", + "title": "ICMP" + }, + { + "dataset": "cassandra", + "index_pattern": "logs-network_traffic.cassandra-*", + "title": "Cassandra" + }, + { + "dataset": "mongodb", + "index_pattern": "logs-network_traffic.mongodb-*", + "title": "MongoDB" + }, + { + "dataset": "thrift", + "index_pattern": "logs-network_traffic.thrift-*", + "title": "Thrift" + }, + { + "dataset": "flow", + "index_pattern": "logs-network_traffic.flow-*", + "title": "Flows" + }, + { + "dataset": "dhcpv4", + "index_pattern": "logs-network_traffic.dhcpv4-*", + "title": "DHCP" + }, + { + "dataset": "pgsql", + "index_pattern": "logs-network_traffic.pgsql-*", + "title": "PostgreSQL" + }, + { + "dataset": "redis", + "index_pattern": "logs-network_traffic.redis-*", + "title": "Redis" + }, + { + "dataset": "dns", + "index_pattern": "logs-network_traffic.dns-*", + "title": "DNS" + }, + { + "dataset": "sip", + "index_pattern": "logs-network_traffic.sip-*", + "title": "SIP" + }, + { + "dataset": "mysql", + "index_pattern": "logs-network_traffic.mysql-*", + "title": "MySQL" + }, + { + "dataset": "amqp", + "index_pattern": "logs-network_traffic.amqp-*", + "title": "AMQP" + }, + { + "dataset": "http", + "index_pattern": "logs-network_traffic.http-*", + "title": "HTTP" + }, + { + "dataset": "memcached", + "index_pattern": "logs-network_traffic.memcached-*", + "title": "Memcached" + } + ], + "elser_embedding": "Network Packet Capture - Capture and analyze network traffic from a host with Elastic Agent. - NFS TLS ICMP Cassandra MongoDB Thrift Flows DHCP PostgreSQL Redis DNS SIP MySQL AMQP HTTP Memcached" + }, + { + "title": "MySQL Enterprise", + "id": "mysql_enterprise", + "description": "Collect audit logs from MySQL Enterprise with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-mysql_enterprise.audit-*", + "title": "MySQL Enterprise Audit Log" + } + ], + "elser_embedding": "MySQL Enterprise - Collect audit logs from MySQL Enterprise with Elastic Agent. - MySQL Enterprise Audit Log" + }, + { + "title": "GitHub", + "id": "github", + "description": "Collect logs from GitHub with Elastic Agent.", + "data_streams": [ + { + "dataset": "dependabot", + "index_pattern": "logs-github.dependabot-*", + "title": "GHAS Dependabot" + }, + { + "dataset": "issues", + "index_pattern": "logs-github.issues-*", + "title": "Github Issue" + }, + { + "dataset": "secret_scanning", + "index_pattern": "logs-github.secret_scanning-*", + "title": "GHAS Secret Scanning" + }, + { + "dataset": "audit", + "index_pattern": "logs-github.audit-*", + "title": "GitHub Audit Logs" + }, + { + "dataset": "code_scanning", + "index_pattern": "logs-github.code_scanning-*", + "title": "GHAS Code Scanning" + } + ], + "elser_embedding": "GitHub - Collect logs from GitHub with Elastic Agent. - GHAS Dependabot Github Issue GHAS Secret Scanning GitHub Audit Logs GHAS Code Scanning" + }, + { + "title": "Microsoft Entra ID Entity Analytics", + "id": "entityanalytics_entra_id", + "description": "Collect identities from Microsoft Entra ID (formerly Azure Active Directory) with Elastic Agent.", + "data_streams": [ + { + "dataset": "device", + "index_pattern": "logs-entityanalytics_entra_id.device-*", + "title": "Microsoft Entra ID Entity Analytics Device Events" + }, + { + "dataset": "user", + "index_pattern": "logs-entityanalytics_entra_id.user-*", + "title": "Microsoft Entra ID Entity Analytics User Events" + }, + { + "dataset": "entity", + "index_pattern": "logs-entityanalytics_entra_id.entity-*", + "title": "Identities" + } + ], + "elser_embedding": "Microsoft Entra ID Entity Analytics - Collect identities from Microsoft Entra ID (formerly Azure Active Directory) with Elastic Agent. - Microsoft Entra ID Entity Analytics Device Events Microsoft Entra ID Entity Analytics User Events Identities" + }, + { + "title": "ThreatConnect", + "id": "ti_threatconnect", + "description": "Collects Indicators from ThreatConnect using the Elastic Agent and saves them as logs inside Elastic", + "data_streams": [ + { + "dataset": "indicator", + "index_pattern": "logs-ti_threatconnect.indicator-*", + "title": "Collect Indicators from ThreatConnect." + } + ], + "elser_embedding": "ThreatConnect - Collects Indicators from ThreatConnect using the Elastic Agent and saves them as logs inside Elastic - Collect Indicators from ThreatConnect." + }, + { + "title": "Microsoft Sentinel", + "id": "microsoft_sentinel", + "description": "Collect logs from Microsoft Sentinel with Elastic Agent.", + "data_streams": [ + { + "dataset": "alert", + "index_pattern": "logs-microsoft_sentinel.alert-*", + "title": "Microsoft Sentinel Alert logs" + }, + { + "dataset": "incident", + "index_pattern": "logs-microsoft_sentinel.incident-*", + "title": "Microsoft Sentinel Incident logs" + }, + { + "dataset": "event", + "index_pattern": "logs-microsoft_sentinel.event-*", + "title": "Collect Events from Microsoft Sentinel." + } + ], + "elser_embedding": "Microsoft Sentinel - Collect logs from Microsoft Sentinel with Elastic Agent. - Microsoft Sentinel Alert logs Microsoft Sentinel Incident logs Collect Events from Microsoft Sentinel." + }, + { + "title": "Check Point", + "id": "checkpoint", + "description": "Collect logs from Check Point with Elastic Agent.", + "data_streams": [ + { + "dataset": "firewall", + "index_pattern": "logs-checkpoint.firewall-*", + "title": "Check Point firewall logs" + } + ], + "elser_embedding": "Check Point - Collect logs from Check Point with Elastic Agent. - Check Point firewall logs" + }, + { + "title": "WatchGuard Firebox", + "id": "watchguard_firebox", + "description": "Collect logs from WatchGuard Firebox with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-watchguard_firebox.log-*", + "title": "WatchGuard Firebox logs" + } + ], + "elser_embedding": "WatchGuard Firebox - Collect logs from WatchGuard Firebox with Elastic Agent. - WatchGuard Firebox logs" + }, + { + "title": "Nagios XI", + "id": "nagios_xi", + "description": "Collect Logs and Metrics from Nagios XI with Elastic Agent.", + "data_streams": [ + { + "dataset": "host", + "index_pattern": "logs-nagios_xi.host-*", + "title": "Host" + }, + { + "dataset": "events", + "index_pattern": "logs-nagios_xi.events-*", + "title": "Events" + }, + { + "dataset": "service", + "index_pattern": "logs-nagios_xi.service-*", + "title": "Service" + } + ], + "elser_embedding": "Nagios XI - Collect Logs and Metrics from Nagios XI with Elastic Agent. - Host Events Service" + }, + { + "title": "Atlassian Jira", + "id": "atlassian_jira", + "description": "Collect logs from Atlassian Jira with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-atlassian_jira.audit-*", + "title": "Jira Audit Logs" + } + ], + "elser_embedding": "Atlassian Jira - Collect logs from Atlassian Jira with Elastic Agent. - Jira Audit Logs" + }, + { + "title": "Snyk", + "id": "snyk", + "description": "Collect logs from Snyk with Elastic Agent.", + "data_streams": [ + { + "dataset": "issues", + "index_pattern": "logs-snyk.issues-*", + "title": "Collect Snyk Issues Data" + }, + { + "dataset": "audit", + "index_pattern": "logs-snyk.audit-*", + "title": "Collect Snyk Audit Logs" + }, + { + "dataset": "audit_logs", + "index_pattern": "logs-snyk.audit_logs-*", + "title": "Collect Snyk Audit Logs" + }, + { + "dataset": "vulnerabilities", + "index_pattern": "logs-snyk.vulnerabilities-*", + "title": "Collect Snyk Vulnerability Data" + } + ], + "elser_embedding": "Snyk - Collect logs from Snyk with Elastic Agent. - Collect Snyk Issues Data Collect Snyk Audit Logs Collect Snyk Audit Logs Collect Snyk Vulnerability Data" + }, + { + "title": "Google Cloud Platform", + "id": "gcp", + "description": "Collect logs and metrics from Google Cloud Platform with Elastic Agent.", + "data_streams": [ + { + "dataset": "compute", + "index_pattern": "logs-gcp.compute-*", + "title": "GCP Compute Metrics" + }, + { + "dataset": "pubsub", + "index_pattern": "logs-gcp.pubsub-*", + "title": "GCP PubSub Metrics" + }, + { + "dataset": "cloudsql_postgresql", + "index_pattern": "logs-gcp.cloudsql_postgresql-*", + "title": "GCP CloudSQL PostgreSQL Metrics" + }, + { + "dataset": "billing", + "index_pattern": "logs-gcp.billing-*", + "title": "GCP Billing Metrics" + }, + { + "dataset": "loadbalancing_metrics", + "index_pattern": "logs-gcp.loadbalancing_metrics-*", + "title": "GCP Load Balancing Metrics" + }, + { + "dataset": "cloudrun_metrics", + "index_pattern": "logs-gcp.cloudrun_metrics-*", + "title": "GCP Cloud Run Metrics" + }, + { + "dataset": "audit", + "index_pattern": "logs-gcp.audit-*", + "title": "Google Cloud Platform (GCP) audit logs" + }, + { + "dataset": "dataproc", + "index_pattern": "logs-gcp.dataproc-*", + "title": "GCP Dataproc Metrics" + }, + { + "dataset": "redis", + "index_pattern": "logs-gcp.redis-*", + "title": "GCP Redis Metrics" + }, + { + "dataset": "cloudsql_mysql", + "index_pattern": "logs-gcp.cloudsql_mysql-*", + "title": "GCP CloudSQL MySQL Metrics" + }, + { + "dataset": "dns", + "index_pattern": "logs-gcp.dns-*", + "title": "Google Cloud Platform (GCP) DNS logs" + }, + { + "dataset": "cloudsql_sqlserver", + "index_pattern": "logs-gcp.cloudsql_sqlserver-*", + "title": "GCP CloudSQL SQL Server Metrics" + }, + { + "dataset": "storage", + "index_pattern": "logs-gcp.storage-*", + "title": "GCP Storage Metrics" + }, + { + "dataset": "gke", + "index_pattern": "logs-gcp.gke-*", + "title": "GCP GKE Metrics" + }, + { + "dataset": "vpcflow", + "index_pattern": "logs-gcp.vpcflow-*", + "title": "Google Cloud Platform (GCP) vpcflow logs" + }, + { + "dataset": "loadbalancing_logs", + "index_pattern": "logs-gcp.loadbalancing_logs-*", + "title": "Google Cloud Platform (GCP) Load Balancing logs" + }, + { + "dataset": "firestore", + "index_pattern": "logs-gcp.firestore-*", + "title": "GCP Firestore Metrics" + }, + { + "dataset": "firewall", + "index_pattern": "logs-gcp.firewall-*", + "title": "Google Cloud Platform (GCP) firewall logs" + } + ], + "elser_embedding": "Google Cloud Platform - Collect logs and metrics from Google Cloud Platform with Elastic Agent. - GCP Compute Metrics GCP PubSub Metrics GCP CloudSQL PostgreSQL Metrics GCP Billing Metrics GCP Load Balancing Metrics GCP Cloud Run Metrics Google Cloud Platform (GCP) audit logs GCP Dataproc Metrics GCP Redis Metrics GCP CloudSQL MySQL Metrics Google Cloud Platform (GCP) DNS logs GCP CloudSQL SQL Server Metrics GCP Storage Metrics GCP GKE Metrics Google Cloud Platform (GCP) vpcflow logs Google Cloud Platform (GCP) Load Balancing logs GCP Firestore Metrics Google Cloud Platform (GCP) firewall logs" + }, + { + "title": "Logstash", + "id": "logstash", + "description": "Collect logs and metrics from Logstash with Elastic Agent.", + "data_streams": [ + { + "dataset": "node_cel", + "index_pattern": "logs-logstash.node_cel-*", + "title": "Logstash Node Stats" + }, + { + "dataset": "pipeline", + "index_pattern": "logs-logstash.pipeline-*", + "title": "Logstash pipeline" + }, + { + "dataset": "plugins", + "index_pattern": "logs-logstash.plugins-*", + "title": "Logstash plugins" + }, + { + "dataset": "node_stats", + "index_pattern": "logs-logstash.node_stats-*", + "title": "Logstash node_stats metrics" + }, + { + "dataset": "slowlog", + "index_pattern": "logs-logstash.slowlog-*", + "title": "logstash slowlog logs" + }, + { + "dataset": "log", + "index_pattern": "logs-logstash.log-*", + "title": "Logstash logs" + }, + { + "dataset": "node", + "index_pattern": "logs-logstash.node-*", + "title": "Logstash node metrics" + } + ], + "elser_embedding": "Logstash - Collect logs and metrics from Logstash with Elastic Agent. - Logstash Node Stats Logstash pipeline Logstash plugins Logstash node_stats metrics logstash slowlog logs Logstash logs Logstash node metrics" + }, + { + "title": "Palo Alto Prisma Access", + "id": "prisma_access", + "description": "Collect logs from Palo Alto Prisma Access with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-prisma_access.event-*", + "title": "Collect Events from Palo Alto Prisma Access" + } + ], + "elser_embedding": "Palo Alto Prisma Access - Collect logs from Palo Alto Prisma Access with Elastic Agent. - Collect Events from Palo Alto Prisma Access" + }, + { + "title": "Barracuda CloudGen Firewall Logs", + "id": "barracuda_cloudgen_firewall", + "description": "Collect logs from Barracuda CloudGen Firewall devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-barracuda_cloudgen_firewall.log-*", + "title": "Barracuda CloudGen Firewall Logs" + } + ], + "elser_embedding": "Barracuda CloudGen Firewall Logs - Collect logs from Barracuda CloudGen Firewall devices with Elastic Agent. - Barracuda CloudGen Firewall Logs" + }, + { + "title": "Jamf Pro", + "id": "jamf_pro", + "description": "Collect logs and inventory data from Jamf Pro with Elastic Agent", + "data_streams": [ + { + "dataset": "inventory", + "index_pattern": "logs-jamf_pro.inventory-*", + "title": "Inventory data" + }, + { + "dataset": "events", + "index_pattern": "logs-jamf_pro.events-*", + "title": "Jamf Pro Events" + } + ], + "elser_embedding": "Jamf Pro - Collect logs and inventory data from Jamf Pro with Elastic Agent - Inventory data Jamf Pro Events" + }, + { + "title": "Fortinet FortiManager Logs", + "id": "fortinet_fortimanager", + "description": "Collect logs from Fortinet FortiManager instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_fortimanager.log-*", + "title": "Collect logs from Fortinet FortiManager" + } + ], + "elser_embedding": "Fortinet FortiManager Logs - Collect logs from Fortinet FortiManager instances with Elastic Agent. - Collect logs from Fortinet FortiManager" + }, + { + "title": "Elastic APM", + "id": "apm", + "description": "Monitor, detect, and diagnose complex application performance issues.", + "data_streams": [], + "elser_embedding": "Elastic APM - Monitor, detect, and diagnose complex application performance issues. - " + }, + { + "title": "AlienVault OTX", + "id": "ti_otx", + "description": "Ingest threat intelligence indicators from AlienVault Open Threat Exchange (OTX) with Elastic Agent.", + "data_streams": [ + { + "dataset": "pulses_subscribed", + "index_pattern": "logs-ti_otx.pulses_subscribed-*", + "title": "Alienvault OTX Subcribed Pulses" + }, + { + "dataset": "threat", + "index_pattern": "logs-ti_otx.threat-*", + "title": "Alienvault OTX logs" + } + ], + "elser_embedding": "AlienVault OTX - Ingest threat intelligence indicators from AlienVault Open Threat Exchange (OTX) with Elastic Agent. - Alienvault OTX Subcribed Pulses Alienvault OTX logs" + }, + { + "title": "Check Point", + "id": "checkpoint", + "description": "Collect logs from Check Point with Elastic Agent.", + "data_streams": [ + { + "dataset": "firewall", + "index_pattern": "logs-checkpoint.firewall-*", + "title": "Check Point firewall logs" + } + ], + "elser_embedding": "Check Point - Collect logs from Check Point with Elastic Agent. - Check Point firewall logs" + }, + { + "title": "Kubernetes OpenTelemetry Assets", + "id": "kubernetes_otel", + "description": "Utilise the pre-built dashboard for OTel-native metrics and events collected from a Kubernetes cluster", + "data_streams": [], + "elser_embedding": "Kubernetes OpenTelemetry Assets - Utilise the pre-built dashboard for OTel-native metrics and events collected from a Kubernetes cluster - " + }, + { + "title": "EclecticIQ", + "id": "ti_eclecticiq", + "description": "Ingest threat intelligence from EclecticIQ with Elastic Agent", + "data_streams": [ + { + "dataset": "threat", + "index_pattern": "logs-ti_eclecticiq.threat-*", + "title": "Poll Outgoing feed" + } + ], + "elser_embedding": "EclecticIQ - Ingest threat intelligence from EclecticIQ with Elastic Agent - Poll Outgoing feed" + }, + { + "title": "Lumos", + "id": "lumos", + "description": "An integration with Lumos to ship your Activity logs to your Elastic instance.", + "data_streams": [ + { + "dataset": "activity_logs", + "index_pattern": "logs-lumos.activity_logs-*", + "title": "Lumos Activity Logs" + } + ], + "elser_embedding": "Lumos - An integration with Lumos to ship your Activity logs to your Elastic instance. - Lumos Activity Logs" + }, + { + "title": "Anomali", + "id": "ti_anomali", + "description": "Ingest threat intelligence indicators from Anomali with Elastic Agent.", + "data_streams": [ + { + "dataset": "threatstream", + "index_pattern": "logs-ti_anomali.threatstream-*", + "title": "Anomali ThreatStream" + }, + { + "dataset": "intelligence", + "index_pattern": "logs-ti_anomali.intelligence-*", + "title": "Anomali ThreatStream" + } + ], + "elser_embedding": "Anomali - Ingest threat intelligence indicators from Anomali with Elastic Agent. - Anomali ThreatStream Anomali ThreatStream" + }, + { + "title": "Jolokia Input", + "id": "jolokia", + "description": "Collects Metrics from Jolokia Agents", + "data_streams": [], + "elser_embedding": "Jolokia Input - Collects Metrics from Jolokia Agents - " + }, + { + "title": "Sysdig", + "id": "sysdig", + "description": "Collect alerts from Sysdig using Elastic Agent.", + "data_streams": [ + { + "dataset": "alerts", + "index_pattern": "logs-sysdig.alerts-*", + "title": "Sysdig" + } + ], + "elser_embedding": "Sysdig - Collect alerts from Sysdig using Elastic Agent. - Sysdig" + }, + { + "title": "Pulse Connect Secure", + "id": "pulse_connect_secure", + "description": "Collect logs from Pulse Connect Secure with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-pulse_connect_secure.log-*", + "title": "Pulse Connect Secure" + } + ], + "elser_embedding": "Pulse Connect Secure - Collect logs from Pulse Connect Secure with Elastic Agent. - Pulse Connect Secure" + }, + { + "title": "Zeek", + "id": "zeek", + "description": "Collect logs from Zeek with Elastic Agent.", + "data_streams": [ + { + "dataset": "x509", + "index_pattern": "logs-zeek.x509-*", + "title": "Zeek x509 logs" + }, + { + "dataset": "software", + "index_pattern": "logs-zeek.software-*", + "title": "Zeek software logs" + }, + { + "dataset": "traceroute", + "index_pattern": "logs-zeek.traceroute-*", + "title": "Zeek traceroute logs" + }, + { + "dataset": "capture_loss", + "index_pattern": "logs-zeek.capture_loss-*", + "title": "Zeek capture_loss logs" + }, + { + "dataset": "smb_cmd", + "index_pattern": "logs-zeek.smb_cmd-*", + "title": "Zeek smb_cmd logs" + }, + { + "dataset": "snmp", + "index_pattern": "logs-zeek.snmp-*", + "title": "Zeek snmp logs" + }, + { + "dataset": "irc", + "index_pattern": "logs-zeek.irc-*", + "title": "Zeek irc logs" + }, + { + "dataset": "intel", + "index_pattern": "logs-zeek.intel-*", + "title": "Zeek intel logs" + }, + { + "dataset": "pe", + "index_pattern": "logs-zeek.pe-*", + "title": "Zeek pe logs" + }, + { + "dataset": "known_services", + "index_pattern": "logs-zeek.known_services-*", + "title": "Zeek Known Services logs" + }, + { + "dataset": "radius", + "index_pattern": "logs-zeek.radius-*", + "title": "Zeek radius logs" + }, + { + "dataset": "modbus", + "index_pattern": "logs-zeek.modbus-*", + "title": "Zeek modbus logs" + }, + { + "dataset": "tunnel", + "index_pattern": "logs-zeek.tunnel-*", + "title": "Zeek tunnel logs" + }, + { + "dataset": "stats", + "index_pattern": "logs-zeek.stats-*", + "title": "Zeek stats logs" + }, + { + "dataset": "smb_files", + "index_pattern": "logs-zeek.smb_files-*", + "title": "Zeek smb_files logs" + }, + { + "dataset": "ocsp", + "index_pattern": "logs-zeek.ocsp-*", + "title": "Zeek ocsp logs" + }, + { + "dataset": "connection", + "index_pattern": "logs-zeek.connection-*", + "title": "Zeek connection logs" + }, + { + "dataset": "kerberos", + "index_pattern": "logs-zeek.kerberos-*", + "title": "Zeek kerberos logs" + }, + { + "dataset": "weird", + "index_pattern": "logs-zeek.weird-*", + "title": "Zeek weird logs" + }, + { + "dataset": "smb_mapping", + "index_pattern": "logs-zeek.smb_mapping-*", + "title": "Zeek smb_mapping logs" + }, + { + "dataset": "signature", + "index_pattern": "logs-zeek.signature-*", + "title": "Zeek signature logs" + }, + { + "dataset": "ntp", + "index_pattern": "logs-zeek.ntp-*", + "title": "Zeek ntp logs" + }, + { + "dataset": "dns", + "index_pattern": "logs-zeek.dns-*", + "title": "Zeek dns logs" + }, + { + "dataset": "dpd", + "index_pattern": "logs-zeek.dpd-*", + "title": "Zeek dpd logs" + }, + { + "dataset": "dhcp", + "index_pattern": "logs-zeek.dhcp-*", + "title": "Zeek dhcp logs" + }, + { + "dataset": "notice", + "index_pattern": "logs-zeek.notice-*", + "title": "Zeek notice logs" + }, + { + "dataset": "files", + "index_pattern": "logs-zeek.files-*", + "title": "Zeek files logs" + }, + { + "dataset": "ntlm", + "index_pattern": "logs-zeek.ntlm-*", + "title": "Zeek ntlm logs" + }, + { + "dataset": "known_certs", + "index_pattern": "logs-zeek.known_certs-*", + "title": "Zeek Known Certs logs" + }, + { + "dataset": "sip", + "index_pattern": "logs-zeek.sip-*", + "title": "Zeek sip logs" + }, + { + "dataset": "rdp", + "index_pattern": "logs-zeek.rdp-*", + "title": "Zeek rdp logs" + }, + { + "dataset": "mysql", + "index_pattern": "logs-zeek.mysql-*", + "title": "Zeek mysql logs" + }, + { + "dataset": "rfb", + "index_pattern": "logs-zeek.rfb-*", + "title": "Zeek rfb logs" + }, + { + "dataset": "ssh", + "index_pattern": "logs-zeek.ssh-*", + "title": "Zeek ssh logs" + }, + { + "dataset": "syslog", + "index_pattern": "logs-zeek.syslog-*", + "title": "Zeek syslog logs" + }, + { + "dataset": "http", + "index_pattern": "logs-zeek.http-*", + "title": "Zeek http logs" + }, + { + "dataset": "ssl", + "index_pattern": "logs-zeek.ssl-*", + "title": "Zeek ssl logs" + }, + { + "dataset": "socks", + "index_pattern": "logs-zeek.socks-*", + "title": "Zeek socks logs" + }, + { + "dataset": "smtp", + "index_pattern": "logs-zeek.smtp-*", + "title": "Zeek smtp logs" + }, + { + "dataset": "ftp", + "index_pattern": "logs-zeek.ftp-*", + "title": "Zeek ftp logs" + }, + { + "dataset": "known_hosts", + "index_pattern": "logs-zeek.known_hosts-*", + "title": "Zeek Known Hosts logs" + }, + { + "dataset": "dnp3", + "index_pattern": "logs-zeek.dnp3-*", + "title": "Zeek dnp3 logs" + }, + { + "dataset": "dce_rpc", + "index_pattern": "logs-zeek.dce_rpc-*", + "title": "Zeek dce_rpc logs" + } + ], + "elser_embedding": "Zeek - Collect logs from Zeek with Elastic Agent. - Zeek x509 logs Zeek software logs Zeek traceroute logs Zeek capture_loss logs Zeek smb_cmd logs Zeek snmp logs Zeek irc logs Zeek intel logs Zeek pe logs Zeek Known Services logs Zeek radius logs Zeek modbus logs Zeek tunnel logs Zeek stats logs Zeek smb_files logs Zeek ocsp logs Zeek connection logs Zeek kerberos logs Zeek weird logs Zeek smb_mapping logs Zeek signature logs Zeek ntp logs Zeek dns logs Zeek dpd logs Zeek dhcp logs Zeek notice logs Zeek files logs Zeek ntlm logs Zeek Known Certs logs Zeek sip logs Zeek rdp logs Zeek mysql logs Zeek rfb logs Zeek ssh logs Zeek syslog logs Zeek http logs Zeek ssl logs Zeek socks logs Zeek smtp logs Zeek ftp logs Zeek Known Hosts logs Zeek dnp3 logs Zeek dce_rpc logs" + }, + { + "title": "CrowdStrike", + "id": "crowdstrike", + "description": "Collect logs from Crowdstrike with Elastic Agent.", + "data_streams": [ + { + "dataset": "fdr", + "index_pattern": "logs-crowdstrike.fdr-*", + "title": "Falcon Data Replicator" + }, + { + "dataset": "host", + "index_pattern": "logs-crowdstrike.host-*", + "title": "Collect Host logs from CrowdStrike." + }, + { + "dataset": "alert", + "index_pattern": "logs-crowdstrike.alert-*", + "title": "Collect Alert logs from CrowdStrike." + }, + { + "dataset": "falcon", + "index_pattern": "logs-crowdstrike.falcon-*", + "title": "Crowdstrike falcon logs" + } + ], + "elser_embedding": "CrowdStrike - Collect logs from Crowdstrike with Elastic Agent. - Falcon Data Replicator Collect Host logs from CrowdStrike. Collect Alert logs from CrowdStrike. Crowdstrike falcon logs" + }, + { + "title": "Fortinet FortiGate Firewall Logs", + "id": "fortinet_fortigate", + "description": "Collect logs from Fortinet FortiGate firewalls with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_fortigate.log-*", + "title": "Fortinet FortiGate logs" + } + ], + "elser_embedding": "Fortinet FortiGate Firewall Logs - Collect logs from Fortinet FortiGate firewalls with Elastic Agent. - Fortinet FortiGate logs" + }, + { + "title": "Active Directory Entity Analytics", + "id": "entityanalytics_ad", + "description": "Collect User Identities from Active Directory Entity with Elastic Agent.", + "data_streams": [ + { + "dataset": "user", + "index_pattern": "logs-entityanalytics_ad.user-*", + "title": "Collect User Identities logs from Active Directory" + } + ], + "elser_embedding": "Active Directory Entity Analytics - Collect User Identities from Active Directory Entity with Elastic Agent. - Collect User Identities logs from Active Directory" + }, + { + "title": "Arista NG Firewall", + "id": "arista_ngfw", + "description": "Collect logs and metrics from Arista NG Firewall.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-arista_ngfw.log-*", + "title": "Firewall Event" + } + ], + "elser_embedding": "Arista NG Firewall - Collect logs and metrics from Arista NG Firewall. - Firewall Event" + }, + { + "title": "Proofpoint TAP", + "id": "proofpoint_tap", + "description": "Collect logs from Proofpoint TAP with Elastic Agent.", + "data_streams": [ + { + "dataset": "message_blocked", + "index_pattern": "logs-proofpoint_tap.message_blocked-*", + "title": "Message Blocked" + }, + { + "dataset": "clicks_blocked", + "index_pattern": "logs-proofpoint_tap.clicks_blocked-*", + "title": "Clicks Blocked" + }, + { + "dataset": "clicks_permitted", + "index_pattern": "logs-proofpoint_tap.clicks_permitted-*", + "title": "Clicks Permitted" + }, + { + "dataset": "message_delivered", + "index_pattern": "logs-proofpoint_tap.message_delivered-*", + "title": "Message Delivered" + } + ], + "elser_embedding": "Proofpoint TAP - Collect logs from Proofpoint TAP with Elastic Agent. - Message Blocked Clicks Blocked Clicks Permitted Message Delivered" + }, + { + "title": "BitDefender", + "id": "bitdefender", + "description": "Ingest BitDefender GravityZone logs and data", + "data_streams": [ + { + "dataset": "push_statistics", + "index_pattern": "logs-bitdefender.push_statistics-*", + "title": "BitDefender GravityZone Push Notification Statistics" + }, + { + "dataset": "push_configuration", + "index_pattern": "logs-bitdefender.push_configuration-*", + "title": "BitDefender GravityZone Push Notification Configuration" + }, + { + "dataset": "push_notifications", + "index_pattern": "logs-bitdefender.push_notifications-*", + "title": "BitDefender GravityZone Push Notifications" + } + ], + "elser_embedding": "BitDefender - Ingest BitDefender GravityZone logs and data - BitDefender GravityZone Push Notification Statistics BitDefender GravityZone Push Notification Configuration BitDefender GravityZone Push Notifications" + }, + { + "title": "Redis", + "id": "redis", + "description": "Collect logs and metrics from Redis servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "keyspace", + "index_pattern": "logs-redis.keyspace-*", + "title": "Redis keyspace metrics" + }, + { + "dataset": "key", + "index_pattern": "logs-redis.key-*", + "title": "Redis key metrics" + }, + { + "dataset": "info", + "index_pattern": "logs-redis.info-*", + "title": "Redis info metrics" + }, + { + "dataset": "slowlog", + "index_pattern": "logs-redis.slowlog-*", + "title": "Redis slow logs" + }, + { + "dataset": "log", + "index_pattern": "logs-redis.log-*", + "title": "Redis application logs" + } + ], + "elser_embedding": "Redis - Collect logs and metrics from Redis servers with Elastic Agent. - Redis keyspace metrics Redis key metrics Redis info metrics Redis slow logs Redis application logs" + }, + { + "title": "Cisco Duo", + "id": "cisco_duo", + "description": "Collect logs from Cisco Duo with Elastic Agent.", + "data_streams": [ + { + "dataset": "summary", + "index_pattern": "logs-cisco_duo.summary-*", + "title": "Cisco Duo summary logs" + }, + { + "dataset": "admin", + "index_pattern": "logs-cisco_duo.admin-*", + "title": "Cisco Duo administrator logs" + }, + { + "dataset": "telephony", + "index_pattern": "logs-cisco_duo.telephony-*", + "title": "Cisco Duo telephony logs (legacy)" + }, + { + "dataset": "telephony_v2", + "index_pattern": "logs-cisco_duo.telephony_v2-*", + "title": "Cisco Duo telephony logs" + }, + { + "dataset": "auth", + "index_pattern": "logs-cisco_duo.auth-*", + "title": "Cisco Duo authentication logs" + }, + { + "dataset": "trust_monitor", + "index_pattern": "logs-cisco_duo.trust_monitor-*", + "title": "Cisco Duo trust monitor logs" + }, + { + "dataset": "activity", + "index_pattern": "logs-cisco_duo.activity-*", + "title": "Cisco Duo activity logs" + }, + { + "dataset": "offline_enrollment", + "index_pattern": "logs-cisco_duo.offline_enrollment-*", + "title": "Cisco Duo offline enrollment logs" + } + ], + "elser_embedding": "Cisco Duo - Collect logs from Cisco Duo with Elastic Agent. - Cisco Duo summary logs Cisco Duo administrator logs Cisco Duo telephony logs (legacy) Cisco Duo telephony logs Cisco Duo authentication logs Cisco Duo trust monitor logs Cisco Duo activity logs Cisco Duo offline enrollment logs" + }, + { + "title": "Elasticsearch", + "id": "elasticsearch", + "description": "Elasticsearch Integration", + "data_streams": [ + { + "dataset": "index_recovery", + "index_pattern": "logs-elasticsearch.index_recovery-*", + "title": "Elasticsearch index_recovery metrics" + }, + { + "dataset": "shard", + "index_pattern": "logs-elasticsearch.shard-*", + "title": "Elasticsearch shard metrics" + }, + { + "dataset": "ingest_pipeline", + "index_pattern": "logs-elasticsearch.ingest_pipeline-*", + "title": "Elasticsearch ingest metrics" + }, + { + "dataset": "enrich", + "index_pattern": "logs-elasticsearch.enrich-*", + "title": "Elasticsearch enrich metrics" + }, + { + "dataset": "audit", + "index_pattern": "logs-elasticsearch.audit-*", + "title": "Elasticsearch audit logs" + }, + { + "dataset": "server", + "index_pattern": "logs-elasticsearch.server-*", + "title": "Elasticsearch server logs" + }, + { + "dataset": "node_stats", + "index_pattern": "logs-elasticsearch.node_stats-*", + "title": "Elasticsearch node_stats metrics" + }, + { + "dataset": "index_summary", + "index_pattern": "logs-elasticsearch.index_summary-*", + "title": "Elasticsearch index_summary metrics" + }, + { + "dataset": "deprecation", + "index_pattern": "logs-elasticsearch.deprecation-*", + "title": "Elasticsearch deprecation logs" + }, + { + "dataset": "index", + "index_pattern": "logs-elasticsearch.index-*", + "title": "Elasticsearch index metrics" + }, + { + "dataset": "slowlog", + "index_pattern": "logs-elasticsearch.slowlog-*", + "title": "Elasticsearch slowlog logs" + }, + { + "dataset": "pending_tasks", + "index_pattern": "logs-elasticsearch.pending_tasks-*", + "title": "Elasticsearch pending_tasks metrics" + }, + { + "dataset": "ccr", + "index_pattern": "logs-elasticsearch.ccr-*", + "title": "Elasticsearch ccr metrics" + }, + { + "dataset": "node", + "index_pattern": "logs-elasticsearch.node-*", + "title": "Elasticsearch node metrics" + }, + { + "dataset": "cluster_stats", + "index_pattern": "logs-elasticsearch.cluster_stats-*", + "title": "Elasticsearch cluster_stats metrics" + }, + { + "dataset": "gc", + "index_pattern": "logs-elasticsearch.gc-*", + "title": "Elasticsearch gc logs" + }, + { + "dataset": "ml_job", + "index_pattern": "logs-elasticsearch.ml_job-*", + "title": "Elasticsearch ml_job metrics" + } + ], + "elser_embedding": "Elasticsearch - Elasticsearch Integration - Elasticsearch index_recovery metrics Elasticsearch shard metrics Elasticsearch ingest metrics Elasticsearch enrich metrics Elasticsearch audit logs Elasticsearch server logs Elasticsearch node_stats metrics Elasticsearch index_summary metrics Elasticsearch deprecation logs Elasticsearch index metrics Elasticsearch slowlog logs Elasticsearch pending_tasks metrics Elasticsearch ccr metrics Elasticsearch node metrics Elasticsearch cluster_stats metrics Elasticsearch gc logs Elasticsearch ml_job metrics" + }, + { + "title": "Universal Profiling Agent", + "id": "profiler_agent", + "description": "Fleet-wide, whole-system, continuous profiling with zero instrumentation.", + "data_streams": [], + "elser_embedding": "Universal Profiling Agent - Fleet-wide, whole-system, continuous profiling with zero instrumentation. - " + }, + { + "title": "Check Point Harmony Email & Collaboration", + "id": "checkpoint_email", + "description": "Collect logs from Check Point Harmony Email & Collaboration with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-checkpoint_email.event-*", + "title": "Check Point Harmony Email & Collaboration Event logs" + } + ], + "elser_embedding": "Check Point Harmony Email & Collaboration - Collect logs from Check Point Harmony Email & Collaboration with Elastic Agent. - Check Point Harmony Email & Collaboration Event logs" + }, + { + "title": "Apache HTTP Server", + "id": "apache", + "description": "Collect logs and metrics from Apache servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-apache.access-*", + "title": "Apache access logs" + }, + { + "dataset": "error", + "index_pattern": "logs-apache.error-*", + "title": "Apache error logs" + }, + { + "dataset": "status", + "index_pattern": "logs-apache.status-*", + "title": "Apache status metrics" + } + ], + "elser_embedding": "Apache HTTP Server - Collect logs and metrics from Apache servers with Elastic Agent. - Apache access logs Apache error logs Apache status metrics" + }, + { + "title": "Istio", + "id": "istio", + "description": "Collect logs and metrics from the service mesh Istio with Elastic Agent.", + "data_streams": [ + { + "dataset": "access_logs", + "index_pattern": "logs-istio.access_logs-*", + "title": "Istio access logs" + }, + { + "dataset": "proxy_metrics", + "index_pattern": "logs-istio.proxy_metrics-*", + "title": "Istio Proxy Metrics" + }, + { + "dataset": "istiod_metrics", + "index_pattern": "logs-istio.istiod_metrics-*", + "title": "Istiod Metrics" + } + ], + "elser_embedding": "Istio - Collect logs and metrics from the service mesh Istio with Elastic Agent. - Istio access logs Istio Proxy Metrics Istiod Metrics" + }, + { + "title": "GCP Metrics Input", + "id": "gcp_metrics", + "description": "GCP Metrics Input", + "data_streams": [], + "elser_embedding": "GCP Metrics Input - GCP Metrics Input - " + }, + { + "title": "Fortinet FortiMail", + "id": "fortinet_fortimail", + "description": "Collect logs from Fortinet FortiMail instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_fortimail.log-*", + "title": "Collect logs from Fortinet FortiMail" + } + ], + "elser_embedding": "Fortinet FortiMail - Collect logs from Fortinet FortiMail instances with Elastic Agent. - Collect logs from Fortinet FortiMail" + }, + { + "title": "Spring Boot", + "id": "spring_boot", + "description": "This Elastic integration collects logs and metrics from Spring Boot integration.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-spring_boot.memory-*", + "title": "Memory Metrics" + }, + { + "dataset": "http_trace", + "index_pattern": "logs-spring_boot.http_trace-*", + "title": "HTTP Trace Metrics" + }, + { + "dataset": "gc", + "index_pattern": "logs-spring_boot.gc-*", + "title": "Garbage Collector (GC) Metrics" + }, + { + "dataset": "threading", + "index_pattern": "logs-spring_boot.threading-*", + "title": "Threading Metrics" + }, + { + "dataset": "audit_events", + "index_pattern": "logs-spring_boot.audit_events-*", + "title": "Audit Events" + } + ], + "elser_embedding": "Spring Boot - This Elastic integration collects logs and metrics from Spring Boot integration. - Memory Metrics HTTP Trace Metrics Garbage Collector (GC) Metrics Threading Metrics Audit Events" + }, + { + "title": "Jamf Compliance Reporter", + "id": "jamf_compliance_reporter", + "description": "Collect logs from Jamf Compliance Reporter with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-jamf_compliance_reporter.log-*", + "title": "Jamf Compliance Reporter logs" + } + ], + "elser_embedding": "Jamf Compliance Reporter - Collect logs from Jamf Compliance Reporter with Elastic Agent. - Jamf Compliance Reporter logs" + }, + { + "title": "SentinelOne", + "id": "sentinel_one", + "description": "Collect logs from SentinelOne with Elastic Agent.", + "data_streams": [ + { + "dataset": "group", + "index_pattern": "logs-sentinel_one.group-*", + "title": "Collect Group logs from SentinelOne" + }, + { + "dataset": "threat", + "index_pattern": "logs-sentinel_one.threat-*", + "title": "Collect Threat logs from SentinelOne" + }, + { + "dataset": "alert", + "index_pattern": "logs-sentinel_one.alert-*", + "title": "Collect Alert logs from SentinelOne" + }, + { + "dataset": "agent", + "index_pattern": "logs-sentinel_one.agent-*", + "title": "Collect Agent logs from SentinelOne" + }, + { + "dataset": "activity", + "index_pattern": "logs-sentinel_one.activity-*", + "title": "Collect Activity logs from SentinelOne" + } + ], + "elser_embedding": "SentinelOne - Collect logs from SentinelOne with Elastic Agent. - Collect Group logs from SentinelOne Collect Threat logs from SentinelOne Collect Alert logs from SentinelOne Collect Agent logs from SentinelOne Collect Activity logs from SentinelOne" + }, + { + "title": "Enterprise Search", + "id": "enterprisesearch", + "description": "Enterprise Search Integration", + "data_streams": [ + { + "dataset": "stats", + "index_pattern": "logs-enterprisesearch.stats-*", + "title": "Enterprise Search stats metrics" + }, + { + "dataset": "health", + "index_pattern": "logs-enterprisesearch.health-*", + "title": "Enterprise Search health metrics" + } + ], + "elser_embedding": "Enterprise Search - Enterprise Search Integration - Enterprise Search stats metrics Enterprise Search health metrics" + }, + { + "title": "Microsoft Exchange Online Message Trace", + "id": "microsoft_exchange_online_message_trace", + "description": "Microsoft Exchange Online Message Trace Integration", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-microsoft_exchange_online_message_trace.log-*", + "title": "Microsoft Exchange Online Message Trace logs" + } + ], + "elser_embedding": "Microsoft Exchange Online Message Trace - Microsoft Exchange Online Message Trace Integration - Microsoft Exchange Online Message Trace logs" + }, + { + "title": "CrowdStrike Falcon Intelligence", + "id": "ti_crowdstrike", + "description": "Collect logs from CrowdStrike Falcon Intelligence with Elastic Agent.", + "data_streams": [ + { + "dataset": "intel", + "index_pattern": "logs-ti_crowdstrike.intel-*", + "title": "Collect Intel logs from CrowdStrike Falcon Intelligence." + }, + { + "dataset": "ioc", + "index_pattern": "logs-ti_crowdstrike.ioc-*", + "title": "Collect IOC logs from CrowdStrike Falcon Intelligence." + } + ], + "elser_embedding": "CrowdStrike Falcon Intelligence - Collect logs from CrowdStrike Falcon Intelligence with Elastic Agent. - Collect Intel logs from CrowdStrike Falcon Intelligence. Collect IOC logs from CrowdStrike Falcon Intelligence." + }, + { + "title": "Auditd Manager", + "id": "auditd_manager", + "description": "The Auditd Manager Integration receives audit events from the Linux Audit Framework that is a part of the Linux kernel.", + "data_streams": [ + { + "dataset": "auditd", + "index_pattern": "logs-auditd_manager.auditd-*", + "title": "Auditd Manager" + } + ], + "elser_embedding": "Auditd Manager - The Auditd Manager Integration receives audit events from the Linux Audit Framework that is a part of the Linux kernel. - Auditd Manager" + }, + { + "title": "Oracle", + "id": "oracle", + "description": "Collect Oracle Audit Log, Performance metrics, Tablespace metrics, Sysmetrics metrics, System statistics metrics, memory metrics from Oracle database.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-oracle.memory-*", + "title": "Memory metrics" + }, + { + "dataset": "performance", + "index_pattern": "logs-oracle.performance-*", + "title": "Oracle performance metrics" + }, + { + "dataset": "database_audit", + "index_pattern": "logs-oracle.database_audit-*", + "title": "Oracle Audit Log" + }, + { + "dataset": "sysmetric", + "index_pattern": "logs-oracle.sysmetric-*", + "title": "Sysmetric related metrics." + }, + { + "dataset": "system_statistics", + "index_pattern": "logs-oracle.system_statistics-*", + "title": "System Statistics" + }, + { + "dataset": "tablespace", + "index_pattern": "logs-oracle.tablespace-*", + "title": "Oracle tablespace metrics" + } + ], + "elser_embedding": "Oracle - Collect Oracle Audit Log, Performance metrics, Tablespace metrics, Sysmetrics metrics, System statistics metrics, memory metrics from Oracle database. - Memory metrics Oracle performance metrics Oracle Audit Log Sysmetric related metrics. System Statistics Oracle tablespace metrics" + }, + { + "title": "Akamai", + "id": "akamai", + "description": "Collect logs from Akamai with Elastic Agent.", + "data_streams": [ + { + "dataset": "siem", + "index_pattern": "logs-akamai.siem-*", + "title": "Akamai SIEM Logs" + } + ], + "elser_embedding": "Akamai - Collect logs from Akamai with Elastic Agent. - Akamai SIEM Logs" + }, + { + "title": "Custom Journald logs", + "id": "journald", + "description": "Collect logs from journald with Elastic Agent.", + "data_streams": [], + "elser_embedding": "Custom Journald logs - Collect logs from journald with Elastic Agent. - " + }, + { + "title": "Universal Profiling Collector", + "id": "profiler_collector", + "description": "Fleet-wide, whole-system, continuous profiling with zero instrumentation.", + "data_streams": [], + "elser_embedding": "Universal Profiling Collector - Fleet-wide, whole-system, continuous profiling with zero instrumentation. - " + }, + { + "title": "Custom API using Common Expression Language", + "id": "cel", + "description": "Collect custom events from an API with Elastic agent", + "data_streams": [], + "elser_embedding": "Custom API using Common Expression Language - Collect custom events from an API with Elastic agent - " + }, + { + "title": "etcd", + "id": "etcd", + "description": "Collect metrics from etcd instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "self", + "index_pattern": "logs-etcd.self-*", + "title": "etcd self metrics" + }, + { + "dataset": "leader", + "index_pattern": "logs-etcd.leader-*", + "title": "etcd leader metrics" + }, + { + "dataset": "store", + "index_pattern": "logs-etcd.store-*", + "title": "etcd store metrics" + }, + { + "dataset": "metrics", + "index_pattern": "logs-etcd.metrics-*", + "title": "etcd v3 metrics" + } + ], + "elser_embedding": "etcd - Collect metrics from etcd instances with Elastic Agent. - etcd self metrics etcd leader metrics etcd store metrics etcd v3 metrics" + }, + { + "title": "Citrix Web App Firewall", + "id": "citrix_waf", + "description": "Ingest events from Citrix Systems Web App Firewall.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-citrix_waf.log-*", + "title": "Cisco ASA logs" + } + ], + "elser_embedding": "Citrix Web App Firewall - Ingest events from Citrix Systems Web App Firewall. - Cisco ASA logs" + }, + { + "title": "Azure OpenAI", + "id": "azure_openai", + "description": "Collects Azure OpenAI Logs and Metrics", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-azure_openai.logs-*", + "title": "Collect Azure OpenAI logs" + }, + { + "dataset": "metrics", + "index_pattern": "logs-azure_openai.metrics-*", + "title": "Collect OpenAI metrics" + } + ], + "elser_embedding": "Azure OpenAI - Collects Azure OpenAI Logs and Metrics - Collect Azure OpenAI logs Collect OpenAI metrics" + }, + { + "title": "Cisco ISE", + "id": "cisco_ise", + "description": "Collect logs from Cisco ISE with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_ise.log-*", + "title": "Cisco ISE logs" + } + ], + "elser_embedding": "Cisco ISE - Collect logs from Cisco ISE with Elastic Agent. - Cisco ISE logs" + }, + { + "title": "Citrix ADC", + "id": "citrix_adc", + "description": "This Elastic integration collects logs and metrics from Citrix ADC product.", + "data_streams": [ + { + "dataset": "vpn", + "index_pattern": "logs-citrix_adc.vpn-*", + "title": "VPN metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-citrix_adc.log-*", + "title": "Citrix ADC logs" + }, + { + "dataset": "service", + "index_pattern": "logs-citrix_adc.service-*", + "title": "Citrix ADC Service metrics" + }, + { + "dataset": "system", + "index_pattern": "logs-citrix_adc.system-*", + "title": "System metrics" + }, + { + "dataset": "interface", + "index_pattern": "logs-citrix_adc.interface-*", + "title": "Interface metrics" + }, + { + "dataset": "lbvserver", + "index_pattern": "logs-citrix_adc.lbvserver-*", + "title": "Load Balancing Virtual Server metrics" + } + ], + "elser_embedding": "Citrix ADC - This Elastic integration collects logs and metrics from Citrix ADC product. - VPN metrics Citrix ADC logs Citrix ADC Service metrics System metrics Interface metrics Load Balancing Virtual Server metrics" + }, + { + "title": "Box Events", + "id": "box_events", + "description": "Collect logs from Box with Elastic Agent", + "data_streams": [ + { + "dataset": "events", + "index_pattern": "logs-box_events.events-*", + "title": "List user and enterprise events" + } + ], + "elser_embedding": "Box Events - Collect logs from Box with Elastic Agent - List user and enterprise events" + }, + { + "title": "Prometheus", + "id": "prometheus", + "description": "Collect metrics from Prometheus servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "query", + "index_pattern": "logs-prometheus.query-*", + "title": "Prometheus query metrics" + }, + { + "dataset": "remote_write", + "index_pattern": "logs-prometheus.remote_write-*", + "title": "Prometheus remote_write metrics" + }, + { + "dataset": "collector", + "index_pattern": "logs-prometheus.collector-*", + "title": "Prometheus collector metrics" + } + ], + "elser_embedding": "Prometheus - Collect metrics from Prometheus servers with Elastic Agent. - Prometheus query metrics Prometheus remote_write metrics Prometheus collector metrics" + }, + { + "title": "Kubernetes", + "id": "kubernetes", + "description": "Collect logs and metrics from Kubernetes clusters with Elastic Agent.", + "data_streams": [ + { + "dataset": "state_resourcequota", + "index_pattern": "logs-kubernetes.state_resourcequota-*", + "title": "Kubernetes ResourceQuota metrics" + }, + { + "dataset": "state_storageclass", + "index_pattern": "logs-kubernetes.state_storageclass-*", + "title": "Kubernetes StorageClass metrics" + }, + { + "dataset": "state_persistentvolume", + "index_pattern": "logs-kubernetes.state_persistentvolume-*", + "title": "Kubernetes PersistentVolume metrics" + }, + { + "dataset": "pod", + "index_pattern": "logs-kubernetes.pod-*", + "title": "Kubernetes Pod metrics" + }, + { + "dataset": "state_container", + "index_pattern": "logs-kubernetes.state_container-*", + "title": "Kubernetes Container metrics" + }, + { + "dataset": "state_service", + "index_pattern": "logs-kubernetes.state_service-*", + "title": "Kubernetes Service metrics" + }, + { + "dataset": "state_replicaset", + "index_pattern": "logs-kubernetes.state_replicaset-*", + "title": "Kubernetes state_replicaset metrics" + }, + { + "dataset": "state_deployment", + "index_pattern": "logs-kubernetes.state_deployment-*", + "title": "Kubernetes Deployment metrics" + }, + { + "dataset": "container", + "index_pattern": "logs-kubernetes.container-*", + "title": "Kubernetes Container metrics" + }, + { + "dataset": "state_cronjob", + "index_pattern": "logs-kubernetes.state_cronjob-*", + "title": "Kubernetes Cronjob metrics" + }, + { + "dataset": "state_persistentvolumeclaim", + "index_pattern": "logs-kubernetes.state_persistentvolumeclaim-*", + "title": "Kubernetes PersistentVolumeClaim metrics" + }, + { + "dataset": "apiserver", + "index_pattern": "logs-kubernetes.apiserver-*", + "title": "Kubernetes API Server metrics" + }, + { + "dataset": "audit_logs", + "index_pattern": "logs-kubernetes.audit_logs-*", + "title": "Kubernetes audit logs" + }, + { + "dataset": "container_logs", + "index_pattern": "logs-kubernetes.container_logs-*", + "title": "Kubernetes container logs" + }, + { + "dataset": "state_namespace", + "index_pattern": "logs-kubernetes.state_namespace-*", + "title": "Kubernetes Namespace metrics" + }, + { + "dataset": "controllermanager", + "index_pattern": "logs-kubernetes.controllermanager-*", + "title": "Kubernetes Controller Manager metrics" + }, + { + "dataset": "state_statefulset", + "index_pattern": "logs-kubernetes.state_statefulset-*", + "title": "Kubernetes StatefulSet metrics" + }, + { + "dataset": "state_pod", + "index_pattern": "logs-kubernetes.state_pod-*", + "title": "Kubernetes Pod metrics" + }, + { + "dataset": "event", + "index_pattern": "logs-kubernetes.event-*", + "title": "Kubernetes Event metrics" + }, + { + "dataset": "node", + "index_pattern": "logs-kubernetes.node-*", + "title": "Kubernetes Node metrics" + }, + { + "dataset": "scheduler", + "index_pattern": "logs-kubernetes.scheduler-*", + "title": "Kubernetes Scheduler metrics" + }, + { + "dataset": "system", + "index_pattern": "logs-kubernetes.system-*", + "title": "Kubernetes System metrics" + }, + { + "dataset": "proxy", + "index_pattern": "logs-kubernetes.proxy-*", + "title": "Kubernetes Proxy metrics" + }, + { + "dataset": "state_node", + "index_pattern": "logs-kubernetes.state_node-*", + "title": "Kubernetes Node metrics" + }, + { + "dataset": "volume", + "index_pattern": "logs-kubernetes.volume-*", + "title": "Kubernetes Volume metrics" + }, + { + "dataset": "state_job", + "index_pattern": "logs-kubernetes.state_job-*", + "title": "Kubernetes Job metrics" + }, + { + "dataset": "state_daemonset", + "index_pattern": "logs-kubernetes.state_daemonset-*", + "title": "Kubernetes Deamonset metrics" + } + ], + "elser_embedding": "Kubernetes - Collect logs and metrics from Kubernetes clusters with Elastic Agent. - Kubernetes ResourceQuota metrics Kubernetes StorageClass metrics Kubernetes PersistentVolume metrics Kubernetes Pod metrics Kubernetes Container metrics Kubernetes Service metrics Kubernetes state_replicaset metrics Kubernetes Deployment metrics Kubernetes Container metrics Kubernetes Cronjob metrics Kubernetes PersistentVolumeClaim metrics Kubernetes API Server metrics Kubernetes audit logs Kubernetes container logs Kubernetes Namespace metrics Kubernetes Controller Manager metrics Kubernetes StatefulSet metrics Kubernetes Pod metrics Kubernetes Event metrics Kubernetes Node metrics Kubernetes Scheduler metrics Kubernetes System metrics Kubernetes Proxy metrics Kubernetes Node metrics Kubernetes Volume metrics Kubernetes Job metrics Kubernetes Deamonset metrics" + }, + { + "title": "Okta Entity Analytics", + "id": "entityanalytics_okta", + "description": "Collect User Identities from Okta with Elastic Agent.", + "data_streams": [ + { + "dataset": "user", + "index_pattern": "logs-entityanalytics_okta.user-*", + "title": "Collect User Identities logs from Okta" + } + ], + "elser_embedding": "Okta Entity Analytics - Collect User Identities from Okta with Elastic Agent. - Collect User Identities logs from Okta" + }, + { + "title": "GCP Vertex AI", + "id": "gcp_vertexai", + "description": "Collect GCP Vertex AI metrics with Elastic Agent", + "data_streams": [ + { + "dataset": "metrics", + "index_pattern": "logs-gcp_vertexai.metrics-*", + "title": "GCP Vertex AI Metrics" + } + ], + "elser_embedding": "GCP Vertex AI - Collect GCP Vertex AI metrics with Elastic Agent - GCP Vertex AI Metrics" + }, + { + "title": "First EPSS", + "id": "first_epss", + "description": "Collect exploit prediction score data from the First EPSS API with Elastic Agent.", + "data_streams": [ + { + "dataset": "vulnerability", + "index_pattern": "logs-first_epss.vulnerability-*", + "title": "Collect EPSS data from First API." + } + ], + "elser_embedding": "First EPSS - Collect exploit prediction score data from the First EPSS API with Elastic Agent. - Collect EPSS data from First API." + }, + { + "title": "Snort", + "id": "snort", + "description": "Collect logs from Snort with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-snort.log-*", + "title": "Snort" + } + ], + "elser_embedding": "Snort - Collect logs from Snort with Elastic Agent. - Snort" + }, + { + "title": "Azure Functions", + "id": "azure_functions", + "description": "Get metrics and logs from Azure Functions", + "data_streams": [ + { + "dataset": "functionapplogs", + "index_pattern": "logs-azure_functions.functionapplogs-*", + "title": "Collect Azure Functions logs" + }, + { + "dataset": "metrics", + "index_pattern": "logs-azure_functions.metrics-*", + "title": "Azure Functions App Metrics" + } + ], + "elser_embedding": "Azure Functions - Get metrics and logs from Azure Functions - Collect Azure Functions logs Azure Functions App Metrics" + }, + { + "title": "SentinelOne Cloud Funnel", + "id": "sentinel_one_cloud_funnel", + "description": "Collect logs from SentinelOne Cloud Funnel with Elastic Agent.", + "data_streams": [ + { + "dataset": "threat_intelligence_indicators", + "index_pattern": "logs-sentinel_one_cloud_funnel.threat_intelligence_indicators-*", + "title": "SentinelOne Cloud Funnel Threat Intelligence Indicator Events" + }, + { + "dataset": "scheduled_task", + "index_pattern": "logs-sentinel_one_cloud_funnel.scheduled_task-*", + "title": "SentinelOne Cloud Funnel Scheduled Task Events" + }, + { + "dataset": "cross_process", + "index_pattern": "logs-sentinel_one_cloud_funnel.cross_process-*", + "title": "SentinelOne Cloud Funnel cross_process Events" + }, + { + "dataset": "url", + "index_pattern": "logs-sentinel_one_cloud_funnel.url-*", + "title": "SentinelOne Cloud Funnel URL Events" + }, + { + "dataset": "file", + "index_pattern": "logs-sentinel_one_cloud_funnel.file-*", + "title": "SentinelOne Cloud Funnel File Events" + }, + { + "dataset": "module", + "index_pattern": "logs-sentinel_one_cloud_funnel.module-*", + "title": "SentinelOne Cloud Funnel Module Events" + }, + { + "dataset": "process", + "index_pattern": "logs-sentinel_one_cloud_funnel.process-*", + "title": "SentinelOne Cloud Funnel Process Events" + }, + { + "dataset": "dns", + "index_pattern": "logs-sentinel_one_cloud_funnel.dns-*", + "title": "SentinelOne Cloud Funnel dns Events" + }, + { + "dataset": "logins", + "index_pattern": "logs-sentinel_one_cloud_funnel.logins-*", + "title": "SentinelOne Cloud Funnel Logins Events" + }, + { + "dataset": "command_script", + "index_pattern": "logs-sentinel_one_cloud_funnel.command_script-*", + "title": "SentinelOne Cloud Funnel command_script Events" + }, + { + "dataset": "indicators", + "index_pattern": "logs-sentinel_one_cloud_funnel.indicators-*", + "title": "SentinelOne Cloud Funnel Indicator Events" + }, + { + "dataset": "event", + "index_pattern": "logs-sentinel_one_cloud_funnel.event-*", + "title": "Collect Event logs from SentinelOne Cloud Funnel." + }, + { + "dataset": "ip", + "index_pattern": "logs-sentinel_one_cloud_funnel.ip-*", + "title": "SentinelOne Cloud Funnel IP Events" + }, + { + "dataset": "registry", + "index_pattern": "logs-sentinel_one_cloud_funnel.registry-*", + "title": "SentinelOne Cloud Funnel Registry Events" + } + ], + "elser_embedding": "SentinelOne Cloud Funnel - Collect logs from SentinelOne Cloud Funnel with Elastic Agent. - SentinelOne Cloud Funnel Threat Intelligence Indicator Events SentinelOne Cloud Funnel Scheduled Task Events SentinelOne Cloud Funnel cross_process Events SentinelOne Cloud Funnel URL Events SentinelOne Cloud Funnel File Events SentinelOne Cloud Funnel Module Events SentinelOne Cloud Funnel Process Events SentinelOne Cloud Funnel dns Events SentinelOne Cloud Funnel Logins Events SentinelOne Cloud Funnel command_script Events SentinelOne Cloud Funnel Indicator Events Collect Event logs from SentinelOne Cloud Funnel. SentinelOne Cloud Funnel IP Events SentinelOne Cloud Funnel Registry Events" + }, + { + "title": "Cisco Meraki", + "id": "cisco_meraki", + "description": "Collect logs from Cisco Meraki with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_meraki.log-*", + "title": "Cisco Meraki logs (via Syslog)" + }, + { + "dataset": "events", + "index_pattern": "logs-cisco_meraki.events-*", + "title": "Cisco Meraki webhook events" + } + ], + "elser_embedding": "Cisco Meraki - Collect logs from Cisco Meraki with Elastic Agent. - Cisco Meraki logs (via Syslog) Cisco Meraki webhook events" + }, + { + "title": "Osquery Manager", + "id": "osquery_manager", + "description": "Deploy Osquery with Elastic Agent, then run and schedule queries in Kibana", + "data_streams": [ + { + "dataset": "result", + "index_pattern": "logs-osquery_manager.result-*", + "title": "Osquery Manager queries" + }, + { + "dataset": "action_responses", + "index_pattern": "logs-osquery_manager.action_responses-*", + "title": "Osquery Manager queries" + } + ], + "elser_embedding": "Osquery Manager - Deploy Osquery with Elastic Agent, then run and schedule queries in Kibana - Osquery Manager queries Osquery Manager queries" + }, + { + "title": "ModSecurity Audit", + "id": "modsecurity", + "description": "Collect logs from ModSecurity with Elastic Agent", + "data_streams": [ + { + "dataset": "auditlog", + "index_pattern": "logs-modsecurity.auditlog-*", + "title": "Modsecurity Audit Log" + } + ], + "elser_embedding": "ModSecurity Audit - Collect logs from ModSecurity with Elastic Agent - Modsecurity Audit Log" + }, + { + "title": "pfSense", + "id": "pfsense", + "description": "Collect logs from pfSense and OPNsense with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-pfsense.log-*", + "title": "pfSense log logs" + } + ], + "elser_embedding": "pfSense - Collect logs from pfSense and OPNsense with Elastic Agent. - pfSense log logs" + }, + { + "title": "Ceph", + "id": "ceph", + "description": "This Elastic integration collects metrics from Ceph instance.", + "data_streams": [ + { + "dataset": "cluster_disk", + "index_pattern": "logs-ceph.cluster_disk-*", + "title": "Cluster Disk metrics" + }, + { + "dataset": "osd_pool_stats", + "index_pattern": "logs-ceph.osd_pool_stats-*", + "title": "OSD Pool Stats" + }, + { + "dataset": "cluster_status", + "index_pattern": "logs-ceph.cluster_status-*", + "title": "Cluster Status metrics" + }, + { + "dataset": "pool_disk", + "index_pattern": "logs-ceph.pool_disk-*", + "title": "Pool Disk metrics" + }, + { + "dataset": "osd_tree", + "index_pattern": "logs-ceph.osd_tree-*", + "title": "OSD Tree metrics" + }, + { + "dataset": "osd_performance", + "index_pattern": "logs-ceph.osd_performance-*", + "title": "OSD Performance metrics" + }, + { + "dataset": "cluster_health", + "index_pattern": "logs-ceph.cluster_health-*", + "title": "Cluster Health metrics" + } + ], + "elser_embedding": "Ceph - This Elastic integration collects metrics from Ceph instance. - Cluster Disk metrics OSD Pool Stats Cluster Status metrics Pool Disk metrics OSD Tree metrics OSD Performance metrics Cluster Health metrics" + }, + { + "title": "Maltiverse", + "id": "ti_maltiverse", + "description": "Ingest threat intelligence indicators from Maltiverse feeds with Elastic Agent", + "data_streams": [ + { + "dataset": "indicator", + "index_pattern": "logs-ti_maltiverse.indicator-*", + "title": "Maltiverse indicator" + } + ], + "elser_embedding": "Maltiverse - Ingest threat intelligence indicators from Maltiverse feeds with Elastic Agent - Maltiverse indicator" + }, + { + "title": "Imperva", + "id": "imperva", + "description": "Collect logs from Imperva devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "securesphere", + "index_pattern": "logs-imperva.securesphere-*", + "title": "Collect logs from Imperva SecureSphere" + } + ], + "elser_embedding": "Imperva - Collect logs from Imperva devices with Elastic Agent. - Collect logs from Imperva SecureSphere" + }, + { + "title": "Linux Metrics", + "id": "linux", + "description": "Collect metrics from Linux servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-linux.memory-*", + "title": "Linux-only memory metrics" + }, + { + "dataset": "socket", + "index_pattern": "logs-linux.socket-*", + "title": "System socket metrics" + }, + { + "dataset": "ksm", + "index_pattern": "logs-linux.ksm-*", + "title": "Kernel Samepage merging metrics" + }, + { + "dataset": "raid", + "index_pattern": "logs-linux.raid-*", + "title": "System raid metrics" + }, + { + "dataset": "conntrack", + "index_pattern": "logs-linux.conntrack-*", + "title": "System conntrack metrics" + }, + { + "dataset": "network_summary", + "index_pattern": "logs-linux.network_summary-*", + "title": "System network_summary metrics" + }, + { + "dataset": "users", + "index_pattern": "logs-linux.users-*", + "title": "System users metrics" + }, + { + "dataset": "service", + "index_pattern": "logs-linux.service-*", + "title": "System service metrics" + }, + { + "dataset": "pageinfo", + "index_pattern": "logs-linux.pageinfo-*", + "title": "System page info metrics" + }, + { + "dataset": "iostat", + "index_pattern": "logs-linux.iostat-*", + "title": "Linux disk iostat metrics" + }, + { + "dataset": "entropy", + "index_pattern": "logs-linux.entropy-*", + "title": "System entropy metrics" + } + ], + "elser_embedding": "Linux Metrics - Collect metrics from Linux servers with Elastic Agent. - Linux-only memory metrics System socket metrics Kernel Samepage merging metrics System raid metrics System conntrack metrics System network_summary metrics System users metrics System service metrics System page info metrics Linux disk iostat metrics System entropy metrics" + }, + { + "title": "Cybereason", + "id": "cybereason", + "description": "Collect logs from Cybereason with Elastic Agent.", + "data_streams": [ + { + "dataset": "logon_session", + "index_pattern": "logs-cybereason.logon_session-*", + "title": "Collect Logon Session logs from Cybereason." + }, + { + "dataset": "poll_malop", + "index_pattern": "logs-cybereason.poll_malop-*", + "title": "Collect Poll Malop logs from Cybereason." + }, + { + "dataset": "suspicions_process", + "index_pattern": "logs-cybereason.suspicions_process-*", + "title": "Collect Suspicions Process logs from Cybereason." + }, + { + "dataset": "malop_process", + "index_pattern": "logs-cybereason.malop_process-*", + "title": "Collect Malop Process logs from Cybereason." + }, + { + "dataset": "malop_connection", + "index_pattern": "logs-cybereason.malop_connection-*", + "title": "Collect Malop Connection logs from Cybereason." + }, + { + "dataset": "malware", + "index_pattern": "logs-cybereason.malware-*", + "title": "Collect Malware logs from Cybereason." + } + ], + "elser_embedding": "Cybereason - Collect logs from Cybereason with Elastic Agent. - Collect Logon Session logs from Cybereason. Collect Poll Malop logs from Cybereason. Collect Suspicions Process logs from Cybereason. Collect Malop Process logs from Cybereason. Collect Malop Connection logs from Cybereason. Collect Malware logs from Cybereason." + }, + { + "title": "Kafka", + "id": "kafka", + "description": "Collect logs and metrics from Kafka servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "broker", + "index_pattern": "logs-kafka.broker-*", + "title": "Kafka broker metrics" + }, + { + "dataset": "consumergroup", + "index_pattern": "logs-kafka.consumergroup-*", + "title": "Kafka consumergroup metrics" + }, + { + "dataset": "partition", + "index_pattern": "logs-kafka.partition-*", + "title": "Kafka partition metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-kafka.log-*", + "title": "Kafka log logs" + } + ], + "elser_embedding": "Kafka - Collect logs and metrics from Kafka servers with Elastic Agent. - Kafka broker metrics Kafka consumergroup metrics Kafka partition metrics Kafka log logs" + }, + { + "title": "Sophos Central", + "id": "sophos_central", + "description": "This Elastic integration collects logs from Sophos Central with Elastic Agent.", + "data_streams": [ + { + "dataset": "alert", + "index_pattern": "logs-sophos_central.alert-*", + "title": "Collect Sophos Central SIEM Alert logs" + }, + { + "dataset": "event", + "index_pattern": "logs-sophos_central.event-*", + "title": "Collect Sophos Central SIEM Events logs" + } + ], + "elser_embedding": "Sophos Central - This Elastic integration collects logs from Sophos Central with Elastic Agent. - Collect Sophos Central SIEM Alert logs Collect Sophos Central SIEM Events logs" + }, + { + "title": "PostgreSQL", + "id": "postgresql", + "description": "Collect logs and metrics from PostgreSQL servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "bgwriter", + "index_pattern": "logs-postgresql.bgwriter-*", + "title": "PostgreSQL bgwriter metrics" + }, + { + "dataset": "database", + "index_pattern": "logs-postgresql.database-*", + "title": "PostgreSQL database metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-postgresql.log-*", + "title": "PostgreSQL logs" + }, + { + "dataset": "statement", + "index_pattern": "logs-postgresql.statement-*", + "title": "PostgreSQL statement metrics" + }, + { + "dataset": "activity", + "index_pattern": "logs-postgresql.activity-*", + "title": "PostgreSQL activity metrics" + } + ], + "elser_embedding": "PostgreSQL - Collect logs and metrics from PostgreSQL servers with Elastic Agent. - PostgreSQL bgwriter metrics PostgreSQL database metrics PostgreSQL logs PostgreSQL statement metrics PostgreSQL activity metrics" + }, + { + "title": "Corelight", + "id": "corelight", + "description": "Collect logs from Corelight with Elastic Agent.", + "data_streams": [], + "elser_embedding": "Corelight - Collect logs from Corelight with Elastic Agent. - " + }, + { + "title": "Threat Intelligence Utilities", + "id": "ti_util", + "description": "Prebuilt Threat Intelligence dashboard for Elastic Security", + "data_streams": [], + "elser_embedding": "Threat Intelligence Utilities - Prebuilt Threat Intelligence dashboard for Elastic Security - " + }, + { + "title": "Imperva Cloud WAF", + "id": "imperva_cloud_waf", + "description": "Collect logs from Imperva Cloud WAF with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-imperva_cloud_waf.event-*", + "title": "Collect Imperva Cloud WAF Events" + } + ], + "elser_embedding": "Imperva Cloud WAF - Collect logs from Imperva Cloud WAF with Elastic Agent. - Collect Imperva Cloud WAF Events" + }, + { + "title": "File Integrity Monitoring", + "id": "fim", + "description": "The File Integrity Monitoring integration reports filesystem changes in real time.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-fim.event-*", + "title": "Filesystem events" + } + ], + "elser_embedding": "File Integrity Monitoring - The File Integrity Monitoring integration reports filesystem changes in real time. - Filesystem events" + }, + { + "title": "Custom Websocket logs", + "id": "websocket", + "description": "Collect custom events from a socket server with Elastic agent.", + "data_streams": [], + "elser_embedding": "Custom Websocket logs - Collect custom events from a socket server with Elastic agent. - " + }, + { + "title": "SpyCloud Enterprise Protection", + "id": "spycloud", + "description": "Collect data from SpyCloud Enterprise Protection with Elastic Agent.", + "data_streams": [ + { + "dataset": "compass", + "index_pattern": "logs-spycloud.compass-*", + "title": "Collect Compass logs from SpyCloud Enterprise Protection." + }, + { + "dataset": "breach_record", + "index_pattern": "logs-spycloud.breach_record-*", + "title": "Collect Breach Record logs from SpyCloud Enterprise Protection." + }, + { + "dataset": "breach_catalog", + "index_pattern": "logs-spycloud.breach_catalog-*", + "title": "Collect Breach Catalog logs from SpyCloud Enterprise Protection." + } + ], + "elser_embedding": "SpyCloud Enterprise Protection - Collect data from SpyCloud Enterprise Protection with Elastic Agent. - Collect Compass logs from SpyCloud Enterprise Protection. Collect Breach Record logs from SpyCloud Enterprise Protection. Collect Breach Catalog logs from SpyCloud Enterprise Protection." + }, + { + "title": "Canva", + "id": "canva", + "description": "Collect logs from Canva with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-canva.audit-*", + "title": "Collect Audit Logs from Canva" + } + ], + "elser_embedding": "Canva - Collect logs from Canva with Elastic Agent. - Collect Audit Logs from Canva" + }, + { + "title": "Microsoft Office 365", + "id": "o365", + "description": "Collect logs from Microsoft Office 365 with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-o365.audit-*", + "title": "Microsoft Office 365 audit logs" + } + ], + "elser_embedding": "Microsoft Office 365 - Collect logs from Microsoft Office 365 with Elastic Agent. - Microsoft Office 365 audit logs" + }, + { + "title": "AWS", + "id": "aws", + "description": "Collect logs and metrics from Amazon Web Services (AWS) with Elastic Agent.", + "data_streams": [ + { + "dataset": "ec2_metrics", + "index_pattern": "logs-aws.ec2_metrics-*", + "title": "AWS EC2 metrics" + }, + { + "dataset": "apigateway_metrics", + "index_pattern": "logs-aws.apigateway_metrics-*", + "title": "AWS API Gateway metrics" + }, + { + "dataset": "ec2_logs", + "index_pattern": "logs-aws.ec2_logs-*", + "title": "AWS EC2 logs" + }, + { + "dataset": "cloudwatch_logs", + "index_pattern": "logs-aws.cloudwatch_logs-*", + "title": "AWS CloudWatch logs" + }, + { + "dataset": "billing", + "index_pattern": "logs-aws.billing-*", + "title": "AWS Billing Metrics" + }, + { + "dataset": "ebs", + "index_pattern": "logs-aws.ebs-*", + "title": "AWS EBS metrics" + }, + { + "dataset": "awshealth", + "index_pattern": "logs-aws.awshealth-*", + "title": "AWS Health" + }, + { + "dataset": "transitgateway", + "index_pattern": "logs-aws.transitgateway-*", + "title": "AWS Transit Gateway metrics" + }, + { + "dataset": "cloudtrail", + "index_pattern": "logs-aws.cloudtrail-*", + "title": "AWS CloudTrail Logs" + }, + { + "dataset": "vpn", + "index_pattern": "logs-aws.vpn-*", + "title": "AWS VPN metrics" + }, + { + "dataset": "sns", + "index_pattern": "logs-aws.sns-*", + "title": "AWS SNS metrics" + }, + { + "dataset": "firewall_metrics", + "index_pattern": "logs-aws.firewall_metrics-*", + "title": "AWS Network Firewall metrics" + }, + { + "dataset": "waf", + "index_pattern": "logs-aws.waf-*", + "title": "AWS WAF logs" + }, + { + "dataset": "emr_metrics", + "index_pattern": "logs-aws.emr_metrics-*", + "title": "AWS EMR metrics" + }, + { + "dataset": "firewall_logs", + "index_pattern": "logs-aws.firewall_logs-*", + "title": "AWS Network Firewall logs" + }, + { + "dataset": "lambda", + "index_pattern": "logs-aws.lambda-*", + "title": "AWS Lambda metrics" + }, + { + "dataset": "securityhub_insights", + "index_pattern": "logs-aws.securityhub_insights-*", + "title": "Collect AWS Security Hub Insights logs from AWS" + }, + { + "dataset": "redshift", + "index_pattern": "logs-aws.redshift-*", + "title": "Amazon Redshift metrics" + }, + { + "dataset": "inspector", + "index_pattern": "logs-aws.inspector-*", + "title": "Collect AWS Inspector logs from AWS" + }, + { + "dataset": "route53_resolver_logs", + "index_pattern": "logs-aws.route53_resolver_logs-*", + "title": "AWS Route 53 Resolver Query Logs" + }, + { + "dataset": "emr_logs", + "index_pattern": "logs-aws.emr_logs-*", + "title": "AWS EMR logs" + }, + { + "dataset": "elb_metrics", + "index_pattern": "logs-aws.elb_metrics-*", + "title": "AWS ELB metrics" + }, + { + "dataset": "s3access", + "index_pattern": "logs-aws.s3access-*", + "title": "AWS s3access logs" + }, + { + "dataset": "securityhub_findings", + "index_pattern": "logs-aws.securityhub_findings-*", + "title": "Collect AWS Security Hub Findings logs from AWS" + }, + { + "dataset": "vpcflow", + "index_pattern": "logs-aws.vpcflow-*", + "title": "AWS vpcflow logs" + }, + { + "dataset": "elb_logs", + "index_pattern": "logs-aws.elb_logs-*", + "title": "AWS ELB logs" + }, + { + "dataset": "kafka_metrics", + "index_pattern": "logs-aws.kafka_metrics-*", + "title": "AWS Kafka metrics" + }, + { + "dataset": "kinesis", + "index_pattern": "logs-aws.kinesis-*", + "title": "AWS Kinesis Data Stream metrics" + }, + { + "dataset": "cloudwatch_metrics", + "index_pattern": "logs-aws.cloudwatch_metrics-*", + "title": "AWS CloudWatch metrics" + }, + { + "dataset": "s3_daily_storage", + "index_pattern": "logs-aws.s3_daily_storage-*", + "title": "AWS S3 daily storage metrics" + }, + { + "dataset": "guardduty", + "index_pattern": "logs-aws.guardduty-*", + "title": "Collect Amazon GuardDuty Findings logs from AWS" + }, + { + "dataset": "rds", + "index_pattern": "logs-aws.rds-*", + "title": "AWS RDS metrics" + }, + { + "dataset": "ecs_metrics", + "index_pattern": "logs-aws.ecs_metrics-*", + "title": "AWS ECS metrics" + }, + { + "dataset": "s3_storage_lens", + "index_pattern": "logs-aws.s3_storage_lens-*", + "title": "AWS S3 Storage Lens metrics" + }, + { + "dataset": "route53_public_logs", + "index_pattern": "logs-aws.route53_public_logs-*", + "title": "AWS Route 53 Public Zone Logs" + }, + { + "dataset": "cloudfront_logs", + "index_pattern": "logs-aws.cloudfront_logs-*", + "title": "AWS CloudFront logs" + }, + { + "dataset": "usage", + "index_pattern": "logs-aws.usage-*", + "title": "AWS usage metrics" + }, + { + "dataset": "dynamodb", + "index_pattern": "logs-aws.dynamodb-*", + "title": "AWS DynamoDB metrics" + }, + { + "dataset": "apigateway_logs", + "index_pattern": "logs-aws.apigateway_logs-*", + "title": "AWS API Gateway logs" + }, + { + "dataset": "s3_request", + "index_pattern": "logs-aws.s3_request-*", + "title": "AWS S3 request metrics" + }, + { + "dataset": "sqs", + "index_pattern": "logs-aws.sqs-*", + "title": "AWS SQS metrics" + }, + { + "dataset": "natgateway", + "index_pattern": "logs-aws.natgateway-*", + "title": "AWS NAT gateway metrics" + } + ], + "elser_embedding": "AWS - Collect logs and metrics from Amazon Web Services (AWS) with Elastic Agent. - AWS EC2 metrics AWS API Gateway metrics AWS EC2 logs AWS CloudWatch logs AWS Billing Metrics AWS EBS metrics AWS Health AWS Transit Gateway metrics AWS CloudTrail Logs AWS VPN metrics AWS SNS metrics AWS Network Firewall metrics AWS WAF logs AWS EMR metrics AWS Network Firewall logs AWS Lambda metrics Collect AWS Security Hub Insights logs from AWS Amazon Redshift metrics Collect AWS Inspector logs from AWS AWS Route 53 Resolver Query Logs AWS EMR logs AWS ELB metrics AWS s3access logs Collect AWS Security Hub Findings logs from AWS AWS vpcflow logs AWS ELB logs AWS Kafka metrics AWS Kinesis Data Stream metrics AWS CloudWatch metrics AWS S3 daily storage metrics Collect Amazon GuardDuty Findings logs from AWS AWS RDS metrics AWS ECS metrics AWS S3 Storage Lens metrics AWS Route 53 Public Zone Logs AWS CloudFront logs AWS usage metrics AWS DynamoDB metrics AWS API Gateway logs AWS S3 request metrics AWS SQS metrics AWS NAT gateway metrics" + }, + { + "title": "Nginx Ingress Controller Logs", + "id": "nginx_ingress_controller", + "description": "Collect Nginx Ingress Controller logs.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-nginx_ingress_controller.access-*", + "title": "Nginx Ingress Controller access logs" + }, + { + "dataset": "error", + "index_pattern": "logs-nginx_ingress_controller.error-*", + "title": "Nginx Ingress Controller error logs" + } + ], + "elser_embedding": "Nginx Ingress Controller Logs - Collect Nginx Ingress Controller logs. - Nginx Ingress Controller access logs Nginx Ingress Controller error logs" + }, + { + "title": "Cisco Umbrella", + "id": "cisco_umbrella", + "description": "Collect logs from Cisco Umbrella with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_umbrella.log-*", + "title": "Cisco Umbrella logs" + } + ], + "elser_embedding": "Cisco Umbrella - Collect logs from Cisco Umbrella with Elastic Agent. - Cisco Umbrella logs" + }, + { + "title": "Cloudflare Logpush", + "id": "cloudflare_logpush", + "description": "Collect and parse logs from Cloudflare API with Elastic Agent.", + "data_streams": [ + { + "dataset": "http_request", + "index_pattern": "logs-cloudflare_logpush.http_request-*", + "title": "Collect HTTP Request logs from Cloudflare" + }, + { + "dataset": "access_request", + "index_pattern": "logs-cloudflare_logpush.access_request-*", + "title": "Collect Access Request logs from Cloudflare" + }, + { + "dataset": "network_session", + "index_pattern": "logs-cloudflare_logpush.network_session-*", + "title": "Collect Zero Trust Network Session logs from Cloudflare" + }, + { + "dataset": "spectrum_event", + "index_pattern": "logs-cloudflare_logpush.spectrum_event-*", + "title": "Collect Spectrum Event logs from Cloudflare" + }, + { + "dataset": "gateway_http", + "index_pattern": "logs-cloudflare_logpush.gateway_http-*", + "title": "Collect Gateway HTTP logs from Cloudflare" + }, + { + "dataset": "casb", + "index_pattern": "logs-cloudflare_logpush.casb-*", + "title": "Collect CASB Findings logs from Cloudflare" + }, + { + "dataset": "magic_ids", + "index_pattern": "logs-cloudflare_logpush.magic_ids-*", + "title": "Collect Magic IDS logs from Cloudflare" + }, + { + "dataset": "workers_trace", + "index_pattern": "logs-cloudflare_logpush.workers_trace-*", + "title": "Collect Workers Trace Event logs from Cloudflare" + }, + { + "dataset": "audit", + "index_pattern": "logs-cloudflare_logpush.audit-*", + "title": "Collect Audit logs from Cloudflare" + }, + { + "dataset": "nel_report", + "index_pattern": "logs-cloudflare_logpush.nel_report-*", + "title": "Collect NEL Report logs from Cloudflare" + }, + { + "dataset": "network_analytics", + "index_pattern": "logs-cloudflare_logpush.network_analytics-*", + "title": "Collect Network Analytics logs from Cloudflare" + }, + { + "dataset": "dns", + "index_pattern": "logs-cloudflare_logpush.dns-*", + "title": "Collect DNS logs from Cloudflare" + }, + { + "dataset": "device_posture", + "index_pattern": "logs-cloudflare_logpush.device_posture-*", + "title": "Collect Device Posture Results logs from Cloudflare" + }, + { + "dataset": "gateway_dns", + "index_pattern": "logs-cloudflare_logpush.gateway_dns-*", + "title": "Collect Gateway DNS logs from Cloudflare" + }, + { + "dataset": "dns_firewall", + "index_pattern": "logs-cloudflare_logpush.dns_firewall-*", + "title": "Collect DNS Firewall logs from Cloudflare" + }, + { + "dataset": "sinkhole_http", + "index_pattern": "logs-cloudflare_logpush.sinkhole_http-*", + "title": "Collect Sinkhole HTTP logs from Cloudflare" + }, + { + "dataset": "firewall_event", + "index_pattern": "logs-cloudflare_logpush.firewall_event-*", + "title": "Collect Firewall Event logs from Cloudflare" + }, + { + "dataset": "gateway_network", + "index_pattern": "logs-cloudflare_logpush.gateway_network-*", + "title": "Collect Gateway Network logs from Cloudflare" + } + ], + "elser_embedding": "Cloudflare Logpush - Collect and parse logs from Cloudflare API with Elastic Agent. - Collect HTTP Request logs from Cloudflare Collect Access Request logs from Cloudflare Collect Zero Trust Network Session logs from Cloudflare Collect Spectrum Event logs from Cloudflare Collect Gateway HTTP logs from Cloudflare Collect CASB Findings logs from Cloudflare Collect Magic IDS logs from Cloudflare Collect Workers Trace Event logs from Cloudflare Collect Audit logs from Cloudflare Collect NEL Report logs from Cloudflare Collect Network Analytics logs from Cloudflare Collect DNS logs from Cloudflare Collect Device Posture Results logs from Cloudflare Collect Gateway DNS logs from Cloudflare Collect DNS Firewall logs from Cloudflare Collect Sinkhole HTTP logs from Cloudflare Collect Firewall Event logs from Cloudflare Collect Gateway Network logs from Cloudflare" + }, + { + "title": "Microsoft DHCP", + "id": "microsoft_dhcp", + "description": "Collect logs from Microsoft DHCP with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-microsoft_dhcp.log-*", + "title": "Microsoft DHCP Logs" + } + ], + "elser_embedding": "Microsoft DHCP - Collect logs from Microsoft DHCP with Elastic Agent. - Microsoft DHCP Logs" + }, + { + "title": "Netskope", + "id": "netskope", + "description": "Collect logs from Netskope with Elastic Agent.", + "data_streams": [ + { + "dataset": "alerts", + "index_pattern": "logs-netskope.alerts-*", + "title": "Alerts" + }, + { + "dataset": "events", + "index_pattern": "logs-netskope.events-*", + "title": "Events" + } + ], + "elser_embedding": "Netskope - Collect logs from Netskope with Elastic Agent. - Alerts Events" + }, + { + "title": "Suricata", + "id": "suricata", + "description": "Collect logs from Suricata with Elastic Agent.", + "data_streams": [ + { + "dataset": "eve", + "index_pattern": "logs-suricata.eve-*", + "title": "Suricata eve logs" + } + ], + "elser_embedding": "Suricata - Collect logs from Suricata with Elastic Agent. - Suricata eve logs" + }, + { + "title": "Custom Azure Logs", + "id": "azure_logs", + "description": "Collect log events from Azure Event Hubs with Elastic Agent", + "data_streams": [], + "elser_embedding": "Custom Azure Logs - Collect log events from Azure Event Hubs with Elastic Agent - " + }, + { + "title": "Zscaler Private Access", + "id": "zscaler_zpa", + "description": "Collect logs from Zscaler Private Access (ZPA) with Elastic Agent.", + "data_streams": [ + { + "dataset": "browser_access", + "index_pattern": "logs-zscaler_zpa.browser_access-*", + "title": "Browser Access Logs" + }, + { + "dataset": "app_connector_status", + "index_pattern": "logs-zscaler_zpa.app_connector_status-*", + "title": "App Connector Status Logs" + }, + { + "dataset": "user_status", + "index_pattern": "logs-zscaler_zpa.user_status-*", + "title": "User Status Logs" + }, + { + "dataset": "audit", + "index_pattern": "logs-zscaler_zpa.audit-*", + "title": "Audit Logs" + }, + { + "dataset": "user_activity", + "index_pattern": "logs-zscaler_zpa.user_activity-*", + "title": "User Activity Logs" + } + ], + "elser_embedding": "Zscaler Private Access - Collect logs from Zscaler Private Access (ZPA) with Elastic Agent. - Browser Access Logs App Connector Status Logs User Status Logs Audit Logs User Activity Logs" + }, + { + "title": "Cisco Aironet", + "id": "cisco_aironet", + "description": "Integration for Cisco Aironet WLC Logs", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_aironet.log-*", + "title": "Cisco Aironet logs" + } + ], + "elser_embedding": "Cisco Aironet - Integration for Cisco Aironet WLC Logs - Cisco Aironet logs" + }, + { + "title": "Collective Intelligence Framework v3", + "id": "ti_cif3", + "description": "Ingest threat indicators from a Collective Intelligence Framework v3 instance with Elastic Agent.", + "data_streams": [ + { + "dataset": "feed", + "index_pattern": "logs-ti_cif3.feed-*", + "title": "CIFv3 Feed" + } + ], + "elser_embedding": "Collective Intelligence Framework v3 - Ingest threat indicators from a Collective Intelligence Framework v3 instance with Elastic Agent. - CIFv3 Feed" + }, + { + "title": "Bitwarden", + "id": "bitwarden", + "description": "Collect logs from Bitwarden with Elastic Agent.", + "data_streams": [ + { + "dataset": "group", + "index_pattern": "logs-bitwarden.group-*", + "title": "Collect Group logs from Bitwarden" + }, + { + "dataset": "policy", + "index_pattern": "logs-bitwarden.policy-*", + "title": "Collect Policy logs from Bitwarden" + }, + { + "dataset": "member", + "index_pattern": "logs-bitwarden.member-*", + "title": "Collect Member logs from Bitwarden" + }, + { + "dataset": "event", + "index_pattern": "logs-bitwarden.event-*", + "title": "Collect Event logs from Bitwarden" + }, + { + "dataset": "collection", + "index_pattern": "logs-bitwarden.collection-*", + "title": "Collect Collection logs from Bitwarden" + } + ], + "elser_embedding": "Bitwarden - Collect logs from Bitwarden with Elastic Agent. - Collect Group logs from Bitwarden Collect Policy logs from Bitwarden Collect Member logs from Bitwarden Collect Event logs from Bitwarden Collect Collection logs from Bitwarden" + }, + { + "title": "Kibana", + "id": "kibana", + "description": "Collect logs and metrics from Kibana with Elastic Agent.", + "data_streams": [ + { + "dataset": "node_actions", + "index_pattern": "logs-kibana.node_actions-*", + "title": "Kibana node_actions metrics" + }, + { + "dataset": "stats", + "index_pattern": "logs-kibana.stats-*", + "title": "Kibana stats metrics" + }, + { + "dataset": "audit", + "index_pattern": "logs-kibana.audit-*", + "title": "kibana audit logs" + }, + { + "dataset": "task_manager_metrics", + "index_pattern": "logs-kibana.task_manager_metrics-*", + "title": "Kibana task manager metrics" + }, + { + "dataset": "cluster_rules", + "index_pattern": "logs-kibana.cluster_rules-*", + "title": "Kibana cluster_rules metrics" + }, + { + "dataset": "background_task_utilization", + "index_pattern": "logs-kibana.background_task_utilization-*", + "title": "Kibana background task utilization metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-kibana.log-*", + "title": "Kibana logs" + }, + { + "dataset": "node_rules", + "index_pattern": "logs-kibana.node_rules-*", + "title": "Kibana node_rules metrics" + }, + { + "dataset": "status", + "index_pattern": "logs-kibana.status-*", + "title": "Kibana status metrics" + }, + { + "dataset": "cluster_actions", + "index_pattern": "logs-kibana.cluster_actions-*", + "title": "Kibana cluster_actions metrics" + } + ], + "elser_embedding": "Kibana - Collect logs and metrics from Kibana with Elastic Agent. - Kibana node_actions metrics Kibana stats metrics kibana audit logs Kibana task manager metrics Kibana cluster_rules metrics Kibana background task utilization metrics Kibana logs Kibana node_rules metrics Kibana status metrics Kibana cluster_actions metrics" + }, + { + "title": "Digital Guardian", + "id": "digital_guardian", + "description": "Collect logs from Digital Guardian with Elastic Agent.", + "data_streams": [ + { + "dataset": "arc", + "index_pattern": "logs-digital_guardian.arc-*", + "title": "Digital Guardian ARC Logs" + } + ], + "elser_embedding": "Digital Guardian - Collect logs from Digital Guardian with Elastic Agent. - Digital Guardian ARC Logs" + }, + { + "title": "MySQL", + "id": "mysql", + "description": "Collect logs and metrics from MySQL servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "performance", + "index_pattern": "logs-mysql.performance-*", + "title": "MySQL performance metrics" + }, + { + "dataset": "error", + "index_pattern": "logs-mysql.error-*", + "title": "MySQL error logs" + }, + { + "dataset": "slowlog", + "index_pattern": "logs-mysql.slowlog-*", + "title": "MySQL slowlog logs" + }, + { + "dataset": "galera_status", + "index_pattern": "logs-mysql.galera_status-*", + "title": "MySQL galera_status metrics" + }, + { + "dataset": "replica_status", + "index_pattern": "logs-mysql.replica_status-*", + "title": "Collect replica status metrics from mysql" + }, + { + "dataset": "status", + "index_pattern": "logs-mysql.status-*", + "title": "MySQL status metrics" + } + ], + "elser_embedding": "MySQL - Collect logs and metrics from MySQL servers with Elastic Agent. - MySQL performance metrics MySQL error logs MySQL slowlog logs MySQL galera_status metrics Collect replica status metrics from mysql MySQL status metrics" + }, + { + "title": "CISA Known Exploited Vulnerabilities", + "id": "cisa_kevs", + "description": "This package allows the ingest of known exploited vulnerabilities according to the Cybersecurity and Infrastructure Security Agency of the United States of America. This information could be used to enrich or track exisiting vulnerabilities that are known to be exploited in the wild.", + "data_streams": [ + { + "dataset": "vulnerability", + "index_pattern": "logs-cisa_kevs.vulnerability-*", + "title": "CISA Known Exploited Vulnerabilities List" + } + ], + "elser_embedding": "CISA Known Exploited Vulnerabilities - This package allows the ingest of known exploited vulnerabilities according to the Cybersecurity and Infrastructure Security Agency of the United States of America. This information could be used to enrich or track exisiting vulnerabilities that are known to be exploited in the wild. - CISA Known Exploited Vulnerabilities List" + }, + { + "title": "StormShield SNS", + "id": "stormshield", + "description": "Stormshield SNS integration.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-stormshield.log-*", + "title": "StormShield SNS logs" + } + ], + "elser_embedding": "StormShield SNS - Stormshield SNS integration. - StormShield SNS logs" + }, + { + "title": "1Password", + "id": "1password", + "description": "Collect logs from 1Password with Elastic Agent.", + "data_streams": [ + { + "dataset": "item_usages", + "index_pattern": "logs-1password.item_usages-*", + "title": "Collect 1Password item usages events" + }, + { + "dataset": "signin_attempts", + "index_pattern": "logs-1password.signin_attempts-*", + "title": "1Password sign-in attempt events" + }, + { + "dataset": "audit_events", + "index_pattern": "logs-1password.audit_events-*", + "title": "Collect 1Password audit events" + } + ], + "elser_embedding": "1Password - Collect logs from 1Password with Elastic Agent. - Collect 1Password item usages events 1Password sign-in attempt events Collect 1Password audit events" + }, + { + "title": "Azure Network Watcher NSG", + "id": "azure_network_watcher_nsg", + "description": "Collect logs from Azure Network Watcher NSG with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-azure_network_watcher_nsg.log-*", + "title": "Collect NSG logs from Azure Network Watcher" + } + ], + "elser_embedding": "Azure Network Watcher NSG - Collect logs from Azure Network Watcher NSG with Elastic Agent. - Collect NSG logs from Azure Network Watcher" + }, + { + "title": "WebSphere Application Server", + "id": "websphere_application_server", + "description": "Collects metrics from IBM WebSphere Application Server with Elastic Agent.", + "data_streams": [ + { + "dataset": "threadpool", + "index_pattern": "logs-websphere_application_server.threadpool-*", + "title": "ThreadPool metrics" + }, + { + "dataset": "servlet", + "index_pattern": "logs-websphere_application_server.servlet-*", + "title": "Servlet metrics" + }, + { + "dataset": "session_manager", + "index_pattern": "logs-websphere_application_server.session_manager-*", + "title": "Session Manager metrics" + }, + { + "dataset": "jdbc", + "index_pattern": "logs-websphere_application_server.jdbc-*", + "title": "JDBC metrics" + } + ], + "elser_embedding": "WebSphere Application Server - Collects metrics from IBM WebSphere Application Server with Elastic Agent. - ThreadPool metrics Servlet metrics Session Manager metrics JDBC metrics" + }, + { + "title": "GitLab", + "id": "gitlab", + "description": "Collect logs from GitLab with Elastic Agent.", + "data_streams": [ + { + "dataset": "sidekiq", + "index_pattern": "logs-gitlab.sidekiq-*", + "title": "GitLab Sidekiq logs" + }, + { + "dataset": "audit", + "index_pattern": "logs-gitlab.audit-*", + "title": "Audit" + }, + { + "dataset": "auth", + "index_pattern": "logs-gitlab.auth-*", + "title": "Auth" + }, + { + "dataset": "application", + "index_pattern": "logs-gitlab.application-*", + "title": "Application" + }, + { + "dataset": "pages", + "index_pattern": "logs-gitlab.pages-*", + "title": "GitLab Pages logs" + }, + { + "dataset": "production", + "index_pattern": "logs-gitlab.production-*", + "title": "GitLab Production logs" + }, + { + "dataset": "api", + "index_pattern": "logs-gitlab.api-*", + "title": "GitLab API logs" + } + ], + "elser_embedding": "GitLab - Collect logs from GitLab with Elastic Agent. - GitLab Sidekiq logs Audit Auth Application GitLab Pages logs GitLab Production logs GitLab API logs" + }, + { + "title": "Custom Logs", + "id": "log", + "description": "Collect custom logs with Elastic Agent.", + "data_streams": [], + "elser_embedding": "Custom Logs - Collect custom logs with Elastic Agent. - " + }, + { + "title": "Tenable Vulnerability Management", + "id": "tenable_io", + "description": "Collect logs from Tenable Vulnerability Management with Elastic Agent.", + "data_streams": [ + { + "dataset": "plugin", + "index_pattern": "logs-tenable_io.plugin-*", + "title": "Collect Plugin logs from Tenable Vulnerability Management" + }, + { + "dataset": "vulnerability", + "index_pattern": "logs-tenable_io.vulnerability-*", + "title": "Collect Vulnerability logs from Tenable Vulnerability Management" + }, + { + "dataset": "scan", + "index_pattern": "logs-tenable_io.scan-*", + "title": "Collect Scan logs from Tenable Vulnerability Management" + }, + { + "dataset": "asset", + "index_pattern": "logs-tenable_io.asset-*", + "title": "Collect Asset data from Tenable Vulnerability Management" + } + ], + "elser_embedding": "Tenable Vulnerability Management - Collect logs from Tenable Vulnerability Management with Elastic Agent. - Collect Plugin logs from Tenable Vulnerability Management Collect Vulnerability logs from Tenable Vulnerability Management Collect Scan logs from Tenable Vulnerability Management Collect Asset data from Tenable Vulnerability Management" + }, + { + "title": "Falco", + "id": "falco", + "description": "Collect events and alerts from Falco using Elastic Agent", + "data_streams": [ + { + "dataset": "alerts", + "index_pattern": "logs-falco.alerts-*", + "title": "Falco Alerts" + } + ], + "elser_embedding": "Falco - Collect events and alerts from Falco using Elastic Agent - Falco Alerts" + }, + { + "title": "Docker", + "id": "docker", + "description": "Collect metrics and logs from Docker instances with Elastic Agent.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-docker.memory-*", + "title": "Docker memory metrics" + }, + { + "dataset": "network", + "index_pattern": "logs-docker.network-*", + "title": "Docker network metrics" + }, + { + "dataset": "image", + "index_pattern": "logs-docker.image-*", + "title": "Docker image metrics" + }, + { + "dataset": "container", + "index_pattern": "logs-docker.container-*", + "title": "Docker container metrics" + }, + { + "dataset": "info", + "index_pattern": "logs-docker.info-*", + "title": "Docker info metrics" + }, + { + "dataset": "container_logs", + "index_pattern": "logs-docker.container_logs-*", + "title": "Docker container logs" + }, + { + "dataset": "diskio", + "index_pattern": "logs-docker.diskio-*", + "title": "Docker diskio metrics" + }, + { + "dataset": "event", + "index_pattern": "logs-docker.event-*", + "title": "Docker event metrics" + }, + { + "dataset": "healthcheck", + "index_pattern": "logs-docker.healthcheck-*", + "title": "Docker healthcheck metrics" + }, + { + "dataset": "cpu", + "index_pattern": "logs-docker.cpu-*", + "title": "Docker cpu metrics" + } + ], + "elser_embedding": "Docker - Collect metrics and logs from Docker instances with Elastic Agent. - Docker memory metrics Docker network metrics Docker image metrics Docker container metrics Docker info metrics Docker container logs Docker diskio metrics Docker event metrics Docker healthcheck metrics Docker cpu metrics" + }, + { + "title": "Elastic Synthetics Dashboards", + "id": "synthetics_dashboards", + "description": "Explore Elastic Synthetics metrics with these dashboards.", + "data_streams": [], + "elser_embedding": "Elastic Synthetics Dashboards - Explore Elastic Synthetics metrics with these dashboards. - " + }, + { + "title": "Azure Billing Metrics", + "id": "azure_billing", + "description": "Collect billing metrics with Elastic Agent.", + "data_streams": [ + { + "dataset": "billing", + "index_pattern": "logs-azure_billing.billing-*", + "title": "Azure Billing Metrics" + } + ], + "elser_embedding": "Azure Billing Metrics - Collect billing metrics with Elastic Agent. - Azure Billing Metrics" + }, + { + "title": "Couchbase", + "id": "couchbase", + "description": "Collect metrics from Couchbase databases with Elastic Agent.", + "data_streams": [ + { + "dataset": "cache", + "index_pattern": "logs-couchbase.cache-*", + "title": "Couchbase Sync Gateway Cache metrics." + }, + { + "dataset": "cbl_replication", + "index_pattern": "logs-couchbase.cbl_replication-*", + "title": "Couchbase Sync Gateway CBL Replications metrics" + }, + { + "dataset": "query_index", + "index_pattern": "logs-couchbase.query_index-*", + "title": "Query Index metrics" + }, + { + "dataset": "xdcr", + "index_pattern": "logs-couchbase.xdcr-*", + "title": "Couchbase XDCR Metrics" + }, + { + "dataset": "miscellaneous", + "index_pattern": "logs-couchbase.miscellaneous-*", + "title": "Couchbase Sync Gateway Delta Sync, Import, Security and GSI views metrics." + }, + { + "dataset": "node", + "index_pattern": "logs-couchbase.node-*", + "title": "Node metrics" + }, + { + "dataset": "resource", + "index_pattern": "logs-couchbase.resource-*", + "title": "Couchbase Sync Gateway Resource Utilization metrics." + }, + { + "dataset": "bucket", + "index_pattern": "logs-couchbase.bucket-*", + "title": "Couchbase bucket metrics" + }, + { + "dataset": "cluster", + "index_pattern": "logs-couchbase.cluster-*", + "title": "Couchbase cluster metrics" + }, + { + "dataset": "database_stats", + "index_pattern": "logs-couchbase.database_stats-*", + "title": "Couchbase Sync Gateway Database Stats metrics." + } + ], + "elser_embedding": "Couchbase - Collect metrics from Couchbase databases with Elastic Agent. - Couchbase Sync Gateway Cache metrics. Couchbase Sync Gateway CBL Replications metrics Query Index metrics Couchbase XDCR Metrics Couchbase Sync Gateway Delta Sync, Import, Security and GSI views metrics. Node metrics Couchbase Sync Gateway Resource Utilization metrics. Couchbase bucket metrics Couchbase cluster metrics Couchbase Sync Gateway Database Stats metrics." + }, + { + "title": "VMware Carbon Black Cloud", + "id": "carbon_black_cloud", + "description": "Collect logs from VMWare Carbon Black Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "watchlist_hit", + "index_pattern": "logs-carbon_black_cloud.watchlist_hit-*", + "title": "Watchlist Hit" + }, + { + "dataset": "asset_vulnerability_summary", + "index_pattern": "logs-carbon_black_cloud.asset_vulnerability_summary-*", + "title": "Asset Vulnerability Summary" + }, + { + "dataset": "endpoint_event", + "index_pattern": "logs-carbon_black_cloud.endpoint_event-*", + "title": "Endpoint Event" + }, + { + "dataset": "audit", + "index_pattern": "logs-carbon_black_cloud.audit-*", + "title": "Audit" + }, + { + "dataset": "alert", + "index_pattern": "logs-carbon_black_cloud.alert-*", + "title": "Alert" + }, + { + "dataset": "alert_v7", + "index_pattern": "logs-carbon_black_cloud.alert_v7-*", + "title": "Alert V7" + } + ], + "elser_embedding": "VMware Carbon Black Cloud - Collect logs from VMWare Carbon Black Cloud with Elastic Agent. - Watchlist Hit Asset Vulnerability Summary Endpoint Event Audit Alert Alert V7" + }, + { + "title": "Universal Profiling Symbolizer", + "id": "profiler_symbolizer", + "description": "Fleet-wide, whole-system, continuous profiling with zero instrumentation.", + "data_streams": [], + "elser_embedding": "Universal Profiling Symbolizer - Fleet-wide, whole-system, continuous profiling with zero instrumentation. - " + }, + { + "title": "Fortinet FortiProxy", + "id": "fortinet_fortiproxy", + "description": "Collect logs from Fortinet FortiProxy with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-fortinet_fortiproxy.log-*", + "title": "Collect logs from Fortinet FortiProxy" + } + ], + "elser_embedding": "Fortinet FortiProxy - Collect logs from Fortinet FortiProxy with Elastic Agent. - Collect logs from Fortinet FortiProxy" + }, + { + "title": "MongoDB Atlas", + "id": "mongodb_atlas", + "description": "This Elastic integration collects logs and metrics from MongoDB Atlas instance.", + "data_streams": [ + { + "dataset": "mongod_database", + "index_pattern": "logs-mongodb_atlas.mongod_database-*", + "title": "Collect Mongod Database logs from MongoDB Atlas" + }, + { + "dataset": "disk", + "index_pattern": "logs-mongodb_atlas.disk-*", + "title": "Collect Disk metrics from MongoDB Atlas" + }, + { + "dataset": "project", + "index_pattern": "logs-mongodb_atlas.project-*", + "title": "Collect Project logs from MongoDB Atlas" + }, + { + "dataset": "process", + "index_pattern": "logs-mongodb_atlas.process-*", + "title": "Collect Process metrics from MongoDB Atlas" + }, + { + "dataset": "alert", + "index_pattern": "logs-mongodb_atlas.alert-*", + "title": "Collect Alert logs from MongoDB Atlas" + }, + { + "dataset": "mongod_audit", + "index_pattern": "logs-mongodb_atlas.mongod_audit-*", + "title": "Collect Mongod Audit logs from MongoDB Atlas" + }, + { + "dataset": "organization", + "index_pattern": "logs-mongodb_atlas.organization-*", + "title": "Collect Organization logs from MongoDB Atlas" + }, + { + "dataset": "hardware", + "index_pattern": "logs-mongodb_atlas.hardware-*", + "title": "Collect Hardware metrics from MongoDB Atlas" + } + ], + "elser_embedding": "MongoDB Atlas - This Elastic integration collects logs and metrics from MongoDB Atlas instance. - Collect Mongod Database logs from MongoDB Atlas Collect Disk metrics from MongoDB Atlas Collect Project logs from MongoDB Atlas Collect Process metrics from MongoDB Atlas Collect Alert logs from MongoDB Atlas Collect Mongod Audit logs from MongoDB Atlas Collect Organization logs from MongoDB Atlas Collect Hardware metrics from MongoDB Atlas" + }, + { + "title": "Zero Networks", + "id": "zeronetworks", + "description": "Zero Networks Logs integration", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-zeronetworks.audit-*", + "title": "Zero Networks Audit Logs" + } + ], + "elser_embedding": "Zero Networks - Zero Networks Logs integration - Zero Networks Audit Logs" + }, + { + "title": "CockroachDB Metrics", + "id": "cockroachdb", + "description": "Collect metrics from CockroachDB servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "status", + "index_pattern": "logs-cockroachdb.status-*", + "title": "Status" + } + ], + "elser_embedding": "CockroachDB Metrics - Collect metrics from CockroachDB servers with Elastic Agent. - Status" + }, + { + "title": "Microsoft Exchange Server", + "id": "microsoft_exchange_server", + "description": "Collect logs from Microsoft Exchange Server with Elastic Agent.", + "data_streams": [ + { + "dataset": "imap4_pop3", + "index_pattern": "logs-microsoft_exchange_server.imap4_pop3-*", + "title": "Exchange Server IMAP4 POP3" + }, + { + "dataset": "httpproxy", + "index_pattern": "logs-microsoft_exchange_server.httpproxy-*", + "title": "Exchange HTTPProxy" + }, + { + "dataset": "smtp", + "index_pattern": "logs-microsoft_exchange_server.smtp-*", + "title": "Exchange SMTP" + }, + { + "dataset": "messagetracking", + "index_pattern": "logs-microsoft_exchange_server.messagetracking-*", + "title": "Exchange Messagetracking" + } + ], + "elser_embedding": "Microsoft Exchange Server - Collect logs from Microsoft Exchange Server with Elastic Agent. - Exchange Server IMAP4 POP3 Exchange HTTPProxy Exchange SMTP Exchange Messagetracking" + }, + { + "title": "Cisco Secure Email Gateway", + "id": "cisco_secure_email_gateway", + "description": "Collect logs from Cisco Secure Email Gateway with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_secure_email_gateway.log-*", + "title": "Cisco Secure Email Gateway logs" + } + ], + "elser_embedding": "Cisco Secure Email Gateway - Collect logs from Cisco Secure Email Gateway with Elastic Agent. - Cisco Secure Email Gateway logs" + }, + { + "title": "Prometheus Input", + "id": "prometheus_input", + "description": "Collects metrics from Prometheus exporter.", + "data_streams": [], + "elser_embedding": "Prometheus Input - Collects metrics from Prometheus exporter. - " + }, + { + "title": "PingOne", + "id": "ping_one", + "description": "Collect logs from PingOne with Elastic-Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-ping_one.audit-*", + "title": "Collect Audit logs from PingOne" + } + ], + "elser_embedding": "PingOne - Collect logs from PingOne with Elastic-Agent. - Collect Audit logs from PingOne" + }, + { + "title": "Squid Proxy", + "id": "squid", + "description": "Collect and parse logs from Squid devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-squid.log-*", + "title": "Squid logs" + } + ], + "elser_embedding": "Squid Proxy - Collect and parse logs from Squid devices with Elastic Agent. - Squid logs" + }, + { + "title": "Zoom", + "id": "zoom", + "description": "Collect logs from Zoom with Elastic Agent.", + "data_streams": [ + { + "dataset": "webhook", + "index_pattern": "logs-zoom.webhook-*", + "title": "Zoom webhook logs" + } + ], + "elser_embedding": "Zoom - Collect logs from Zoom with Elastic Agent. - Zoom webhook logs" + }, + { + "title": "Auth0", + "id": "auth0", + "description": "Collect logs from Auth0 with Elastic Agent.", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-auth0.logs-*", + "title": "Auth0 logs" + } + ], + "elser_embedding": "Auth0 - Collect logs from Auth0 with Elastic Agent. - Auth0 logs" + }, + { + "title": "Tomcat NetWitness Logs", + "id": "tomcat", + "description": "Collect and parse logs from Apache Tomcat servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-tomcat.log-*", + "title": "Apache Tomcat logs" + } + ], + "elser_embedding": "Tomcat NetWitness Logs - Collect and parse logs from Apache Tomcat servers with Elastic Agent. - Apache Tomcat logs" + }, + { + "title": "Auditd Logs", + "id": "auditd", + "description": "Collect logs from Linux audit daemon with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-auditd.log-*", + "title": "Auditd logs" + } + ], + "elser_embedding": "Auditd Logs - Collect logs from Linux audit daemon with Elastic Agent. - Auditd logs" + }, + { + "title": "SQL Input", + "id": "sql", + "description": "Collects Metrics by Quering on SQL Databases", + "data_streams": [], + "elser_embedding": "SQL Input - Collects Metrics by Quering on SQL Databases - " + }, + { + "title": "Azure Frontdoor", + "id": "azure_frontdoor", + "description": "This Elastic integration collects logs from Azure Frontdoor.", + "data_streams": [ + { + "dataset": "access", + "index_pattern": "logs-azure_frontdoor.access-*", + "title": "FrontDoor Access" + }, + { + "dataset": "waf", + "index_pattern": "logs-azure_frontdoor.waf-*", + "title": "FrontDoor WAF" + } + ], + "elser_embedding": "Azure Frontdoor - This Elastic integration collects logs from Azure Frontdoor. - FrontDoor Access FrontDoor WAF" + }, + { + "title": "Amazon Data Firehose", + "id": "awsfirehose", + "description": "Stream logs and metrics from Amazon Data Firehose into Elastic Cloud.", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-awsfirehose.logs-*", + "title": "Logs from Amazon Data Firehose" + }, + { + "dataset": "metrics", + "index_pattern": "logs-awsfirehose.metrics-*", + "title": "Metrics ingested from Amazon Data Firehose" + } + ], + "elser_embedding": "Amazon Data Firehose - Stream logs and metrics from Amazon Data Firehose into Elastic Cloud. - Logs from Amazon Data Firehose Metrics ingested from Amazon Data Firehose" + }, + { + "title": "Zscaler Internet Access", + "id": "zscaler_zia", + "description": "Collect logs from Zscaler Internet Access (ZIA) with Elastic Agent.", + "data_streams": [ + { + "dataset": "sandbox_report", + "index_pattern": "logs-zscaler_zia.sandbox_report-*", + "title": "Sandbox Report Logs" + }, + { + "dataset": "tunnel", + "index_pattern": "logs-zscaler_zia.tunnel-*", + "title": "Tunnel Logs" + }, + { + "dataset": "audit", + "index_pattern": "logs-zscaler_zia.audit-*", + "title": "Audit Logs" + }, + { + "dataset": "dns", + "index_pattern": "logs-zscaler_zia.dns-*", + "title": "DNS logs" + }, + { + "dataset": "web", + "index_pattern": "logs-zscaler_zia.web-*", + "title": "Web Logs" + }, + { + "dataset": "endpoint_dlp", + "index_pattern": "logs-zscaler_zia.endpoint_dlp-*", + "title": "Endpoint DLP Logs" + }, + { + "dataset": "alerts", + "index_pattern": "logs-zscaler_zia.alerts-*", + "title": "Alerts" + }, + { + "dataset": "firewall", + "index_pattern": "logs-zscaler_zia.firewall-*", + "title": "Firewall Logs" + } + ], + "elser_embedding": "Zscaler Internet Access - Collect logs from Zscaler Internet Access (ZIA) with Elastic Agent. - Sandbox Report Logs Tunnel Logs Audit Logs DNS logs Web Logs Endpoint DLP Logs Alerts Firewall Logs" + }, + { + "title": "Broadcom ProxySG", + "id": "proxysg", + "description": "Collect access logs from Broadcom ProxySG with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-proxysg.log-*", + "title": "ProxySG Access Logs" + } + ], + "elser_embedding": "Broadcom ProxySG - Collect access logs from Broadcom ProxySG with Elastic Agent. - ProxySG Access Logs" + }, + { + "title": "Juniper SRX", + "id": "juniper_srx", + "description": "Collect logs from Juniper SRX devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-juniper_srx.log-*", + "title": "Juniper SRX logs" + } + ], + "elser_embedding": "Juniper SRX - Collect logs from Juniper SRX devices with Elastic Agent. - Juniper SRX logs" + }, + { + "title": "ServiceNow", + "id": "servicenow", + "description": "Collect logs from ServiceNow with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-servicenow.event-*", + "title": "Event" + } + ], + "elser_embedding": "ServiceNow - Collect logs from ServiceNow with Elastic Agent. - Event" + }, + { + "title": "Defend for Containers", + "id": "cloud_defend", + "description": "Elastic Defend for Containers (BETA) provides cloud-native runtime protections for containerized environments.", + "data_streams": [ + { + "dataset": "heartbeat", + "index_pattern": "logs-cloud_defend.heartbeat-*", + "title": "Cloud Defend Liveness Heartbeat" + }, + { + "dataset": "file", + "index_pattern": "logs-cloud_defend.file-*", + "title": "File telemetry" + }, + { + "dataset": "process", + "index_pattern": "logs-cloud_defend.process-*", + "title": "Process telemetry" + }, + { + "dataset": "metrics", + "index_pattern": "logs-cloud_defend.metrics-*", + "title": "Cloud defend metrics" + }, + { + "dataset": "alerts", + "index_pattern": "logs-cloud_defend.alerts-*", + "title": "alerts" + } + ], + "elser_embedding": "Defend for Containers - Elastic Defend for Containers (BETA) provides cloud-native runtime protections for containerized environments. - Cloud Defend Liveness Heartbeat File telemetry Process telemetry Cloud defend metrics alerts" + }, + { + "title": "authentik", + "id": "authentik", + "description": "Collect logs from authentik with Elastic Agent.", + "data_streams": [ + { + "dataset": "group", + "index_pattern": "logs-authentik.group-*", + "title": "authentik group logs" + }, + { + "dataset": "event", + "index_pattern": "logs-authentik.event-*", + "title": "authentik event logs" + }, + { + "dataset": "user", + "index_pattern": "logs-authentik.user-*", + "title": "authentik user logs" + } + ], + "elser_embedding": "authentik - Collect logs from authentik with Elastic Agent. - authentik group logs authentik event logs authentik user logs" + }, + { + "title": "Wiz", + "id": "wiz", + "description": "Collect logs from Wiz with Elastic Agent.", + "data_streams": [ + { + "dataset": "issue", + "index_pattern": "logs-wiz.issue-*", + "title": "Collect Issue logs from Wiz." + }, + { + "dataset": "vulnerability", + "index_pattern": "logs-wiz.vulnerability-*", + "title": "Collect Vulnerability logs from Wiz." + }, + { + "dataset": "audit", + "index_pattern": "logs-wiz.audit-*", + "title": "Collect Audit logs from Wiz." + }, + { + "dataset": "cloud_configuration_finding", + "index_pattern": "logs-wiz.cloud_configuration_finding-*", + "title": "Collet Cloud Configuration Finding logs from Wiz." + } + ], + "elser_embedding": "Wiz - Collect logs from Wiz with Elastic Agent. - Collect Issue logs from Wiz. Collect Vulnerability logs from Wiz. Collect Audit logs from Wiz. Collet Cloud Configuration Finding logs from Wiz." + }, + { + "title": "Mattermost", + "id": "mattermost", + "description": "Collect logs from Mattermost with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-mattermost.audit-*", + "title": "Audit Logs" + } + ], + "elser_embedding": "Mattermost - Collect logs from Mattermost with Elastic Agent. - Audit Logs" + }, + { + "title": "Teleport", + "id": "teleport", + "description": "Collect logs from Teleport with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-teleport.audit-*", + "title": "Teleport audit logs" + } + ], + "elser_embedding": "Teleport - Collect logs from Teleport with Elastic Agent. - Teleport audit logs" + }, + { + "title": "Fleet Server", + "id": "fleet_server", + "description": "Centrally manage Elastic Agents with the Fleet Server integration.", + "data_streams": [ + { + "dataset": "agent_versions_metrics", + "index_pattern": "logs-fleet_server.agent_versions_metrics-*", + "title": "Fleet Agent Versions" + }, + { + "dataset": "agent_status_metrics", + "index_pattern": "logs-fleet_server.agent_status_metrics-*", + "title": "Fleet Agent Status" + }, + { + "dataset": "output_health_logs", + "index_pattern": "logs-fleet_server.output_health_logs-*", + "title": "Output Health" + } + ], + "elser_embedding": "Fleet Server - Centrally manage Elastic Agents with the Fleet Server integration. - Fleet Agent Versions Fleet Agent Status Output Health" + }, + { + "title": "Cisco Secure Endpoint", + "id": "cisco_secure_endpoint", + "description": "Collect logs from Cisco Secure Endpoint (AMP) with Elastic Agent.", + "data_streams": [ + { + "dataset": "event", + "index_pattern": "logs-cisco_secure_endpoint.event-*", + "title": "Cisco Secure Endpoint logs" + } + ], + "elser_embedding": "Cisco Secure Endpoint - Collect logs from Cisco Secure Endpoint (AMP) with Elastic Agent. - Cisco Secure Endpoint logs" + }, + { + "title": "Iptables", + "id": "iptables", + "description": "Collect logs from Iptables with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-iptables.log-*", + "title": "Iptables log logs" + } + ], + "elser_embedding": "Iptables - Collect logs from Iptables with Elastic Agent. - Iptables log logs" + }, + { + "title": "Google Workspace", + "id": "google_workspace", + "description": "Collect logs from Google Workspace with Elastic Agent.", + "data_streams": [ + { + "dataset": "user_accounts", + "index_pattern": "logs-google_workspace.user_accounts-*", + "title": "User accounts logs" + }, + { + "dataset": "device", + "index_pattern": "logs-google_workspace.device-*", + "title": "Device logs" + }, + { + "dataset": "admin", + "index_pattern": "logs-google_workspace.admin-*", + "title": "Admin logs" + }, + { + "dataset": "gcp", + "index_pattern": "logs-google_workspace.gcp-*", + "title": "GCP logs" + }, + { + "dataset": "group_enterprise", + "index_pattern": "logs-google_workspace.group_enterprise-*", + "title": "Group Enterprise logs" + }, + { + "dataset": "login", + "index_pattern": "logs-google_workspace.login-*", + "title": "Login logs" + }, + { + "dataset": "access_transparency", + "index_pattern": "logs-google_workspace.access_transparency-*", + "title": "Access Transparency logs" + }, + { + "dataset": "alert", + "index_pattern": "logs-google_workspace.alert-*", + "title": "Collect Alert logs from Google Workspace" + }, + { + "dataset": "context_aware_access", + "index_pattern": "logs-google_workspace.context_aware_access-*", + "title": "Context Aware Access logs" + }, + { + "dataset": "token", + "index_pattern": "logs-google_workspace.token-*", + "title": "Token logs" + }, + { + "dataset": "drive", + "index_pattern": "logs-google_workspace.drive-*", + "title": "Drive logs" + }, + { + "dataset": "groups", + "index_pattern": "logs-google_workspace.groups-*", + "title": "Groups logs" + }, + { + "dataset": "saml", + "index_pattern": "logs-google_workspace.saml-*", + "title": "SAML logs" + }, + { + "dataset": "rules", + "index_pattern": "logs-google_workspace.rules-*", + "title": "Rules logs" + } + ], + "elser_embedding": "Google Workspace - Collect logs from Google Workspace with Elastic Agent. - User accounts logs Device logs Admin logs GCP logs Group Enterprise logs Login logs Access Transparency logs Collect Alert logs from Google Workspace Context Aware Access logs Token logs Drive logs Groups logs SAML logs Rules logs" + }, + { + "title": "VMware Carbon Black EDR", + "id": "carbonblack_edr", + "description": "Collect logs from VMware Carbon Black EDR with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-carbonblack_edr.log-*", + "title": "Carbon Black EDR logs" + } + ], + "elser_embedding": "VMware Carbon Black EDR - Collect logs from VMware Carbon Black EDR with Elastic Agent. - Carbon Black EDR logs" + }, + { + "title": "Mimecast", + "id": "mimecast", + "description": "Collect logs from Mimecast with Elastic Agent.", + "data_streams": [ + { + "dataset": "dlp_logs", + "index_pattern": "logs-mimecast.dlp_logs-*", + "title": "DLP Mimecast Logs" + }, + { + "dataset": "ttp_url_logs", + "index_pattern": "logs-mimecast.ttp_url_logs-*", + "title": "TTP URL Logs" + }, + { + "dataset": "siem_logs", + "index_pattern": "logs-mimecast.siem_logs-*", + "title": "SIEM Mimecast Logs" + }, + { + "dataset": "message_release_logs", + "index_pattern": "logs-mimecast.message_release_logs-*", + "title": "Mimecast Message Release" + }, + { + "dataset": "ttp_ip_logs", + "index_pattern": "logs-mimecast.ttp_ip_logs-*", + "title": "TTP Impersonation Mimecast Logs" + }, + { + "dataset": "audit_events", + "index_pattern": "logs-mimecast.audit_events-*", + "title": "Audit Events Mimecast Logs" + }, + { + "dataset": "ttp_ap_logs", + "index_pattern": "logs-mimecast.ttp_ap_logs-*", + "title": "TTP Attachment Logs" + }, + { + "dataset": "archive_search_logs", + "index_pattern": "logs-mimecast.archive_search_logs-*", + "title": "Archive Search Mimecast Logs" + }, + { + "dataset": "threat_intel_malware_grid", + "index_pattern": "logs-mimecast.threat_intel_malware_grid-*", + "title": "Threat Intel Feed - Malware Grid" + }, + { + "dataset": "threat_intel_malware_customer", + "index_pattern": "logs-mimecast.threat_intel_malware_customer-*", + "title": "Threat Intel Feed - Malware Customer" + } + ], + "elser_embedding": "Mimecast - Collect logs from Mimecast with Elastic Agent. - DLP Mimecast Logs TTP URL Logs SIEM Mimecast Logs Mimecast Message Release TTP Impersonation Mimecast Logs Audit Events Mimecast Logs TTP Attachment Logs Archive Search Mimecast Logs Threat Intel Feed - Malware Grid Threat Intel Feed - Malware Customer" + }, + { + "title": "Oracle WebLogic", + "id": "oracle_weblogic", + "description": "Collect logs and metrics from Oracle WebLogic with Elastic Agent.", + "data_streams": [ + { + "dataset": "managed_server", + "index_pattern": "logs-oracle_weblogic.managed_server-*", + "title": "Managed Server logs" + }, + { + "dataset": "access", + "index_pattern": "logs-oracle_weblogic.access-*", + "title": "Access logs" + }, + { + "dataset": "threadpool", + "index_pattern": "logs-oracle_weblogic.threadpool-*", + "title": "Collect Oracle WebLogic ThreadPool metrics" + }, + { + "dataset": "deployed_application", + "index_pattern": "logs-oracle_weblogic.deployed_application-*", + "title": "Collect Oracle WebLogic Deployed Application metrics" + }, + { + "dataset": "admin_server", + "index_pattern": "logs-oracle_weblogic.admin_server-*", + "title": "Admin Server logs" + }, + { + "dataset": "domain", + "index_pattern": "logs-oracle_weblogic.domain-*", + "title": "Domain logs" + } + ], + "elser_embedding": "Oracle WebLogic - Collect logs and metrics from Oracle WebLogic with Elastic Agent. - Managed Server logs Access logs Collect Oracle WebLogic ThreadPool metrics Collect Oracle WebLogic Deployed Application metrics Admin Server logs Domain logs" + }, + { + "title": "System Audit", + "id": "system_audit", + "description": "Collect various logs & metrics from System Audit modules with Elastic Agent.", + "data_streams": [ + { + "dataset": "package", + "index_pattern": "logs-system_audit.package-*", + "title": "System Audit - [Package]" + } + ], + "elser_embedding": "System Audit - Collect various logs & metrics from System Audit modules with Elastic Agent. - System Audit - [Package]" + }, + { + "title": "Salesforce", + "id": "salesforce", + "description": "Collect logs from Salesforce instances using the Elastic Agent. This integration enables monitoring and analysis of various Salesforce logs, including Login, Logout, Setup Audit Trail, and Apex execution logs. Gain insights into user activity, security events, and application performance.\n", + "data_streams": [ + { + "dataset": "setupaudittrail", + "index_pattern": "logs-salesforce.setupaudittrail-*", + "title": "Salesforce setupaudittrail logs" + }, + { + "dataset": "login", + "index_pattern": "logs-salesforce.login-*", + "title": "Salesforce login logs" + }, + { + "dataset": "logout", + "index_pattern": "logs-salesforce.logout-*", + "title": "Salesforce logout logs" + }, + { + "dataset": "apex", + "index_pattern": "logs-salesforce.apex-*", + "title": "Salesforce Apex logs" + } + ], + "elser_embedding": "Salesforce - Collect logs from Salesforce instances using the Elastic Agent. This integration enables monitoring and analysis of various Salesforce logs, including Login, Logout, Setup Audit Trail, and Apex execution logs. Gain insights into user activity, security events, and application performance.\n - Salesforce setupaudittrail logs Salesforce login logs Salesforce logout logs Salesforce Apex logs" + }, + { + "title": "Azure Application Insights Metrics Overview", + "id": "azure_application_insights", + "description": "Collect application insights metrics from Azure Monitor with Elastic Agent.", + "data_streams": [ + { + "dataset": "app_insights", + "index_pattern": "logs-azure_application_insights.app_insights-*", + "title": "Azure Application Insights" + }, + { + "dataset": "app_state", + "index_pattern": "logs-azure_application_insights.app_state-*", + "title": "Azure Application State" + } + ], + "elser_embedding": "Azure Application Insights Metrics Overview - Collect application insights metrics from Azure Monitor with Elastic Agent. - Azure Application Insights Azure Application State" + }, + { + "title": "ForgeRock", + "id": "forgerock", + "description": "Collect audit logs from ForgeRock with Elastic Agent.", + "data_streams": [ + { + "dataset": "idm_sync", + "index_pattern": "logs-forgerock.idm_sync-*", + "title": "IDM-Sync audit logs" + }, + { + "dataset": "idm_core", + "index_pattern": "logs-forgerock.idm_core-*", + "title": "IDM-Core debug logs" + }, + { + "dataset": "am_access", + "index_pattern": "logs-forgerock.am_access-*", + "title": "AM-Access audit logs" + }, + { + "dataset": "idm_activity", + "index_pattern": "logs-forgerock.idm_activity-*", + "title": "IDM-Activity audit logs" + }, + { + "dataset": "idm_config", + "index_pattern": "logs-forgerock.idm_config-*", + "title": "IDM-Config audit logs" + }, + { + "dataset": "am_config", + "index_pattern": "logs-forgerock.am_config-*", + "title": "AM-Config audit logs" + }, + { + "dataset": "am_activity", + "index_pattern": "logs-forgerock.am_activity-*", + "title": "AM-Activity audit logs" + }, + { + "dataset": "am_authentication", + "index_pattern": "logs-forgerock.am_authentication-*", + "title": "AM-Authentication audit logs" + }, + { + "dataset": "idm_authentication", + "index_pattern": "logs-forgerock.idm_authentication-*", + "title": "IDM-Authentication audit logs" + }, + { + "dataset": "idm_access", + "index_pattern": "logs-forgerock.idm_access-*", + "title": "IDM-Access audit logs" + }, + { + "dataset": "am_core", + "index_pattern": "logs-forgerock.am_core-*", + "title": "AM-Core debug logs" + } + ], + "elser_embedding": "ForgeRock - Collect audit logs from ForgeRock with Elastic Agent. - IDM-Sync audit logs IDM-Core debug logs AM-Access audit logs IDM-Activity audit logs IDM-Config audit logs AM-Config audit logs AM-Activity audit logs AM-Authentication audit logs IDM-Authentication audit logs IDM-Access audit logs AM-Core debug logs" + }, + { + "title": "Tenable.sc", + "id": "tenable_sc", + "description": "Collect logs from Tenable.sc with Elastic Agent.\n", + "data_streams": [ + { + "dataset": "plugin", + "index_pattern": "logs-tenable_sc.plugin-*", + "title": "Tenable.sc plugin logs" + }, + { + "dataset": "vulnerability", + "index_pattern": "logs-tenable_sc.vulnerability-*", + "title": "Tenable.sc vulnerability logs" + }, + { + "dataset": "asset", + "index_pattern": "logs-tenable_sc.asset-*", + "title": "Tenable.sc asset logs" + } + ], + "elser_embedding": "Tenable.sc - Collect logs from Tenable.sc with Elastic Agent.\n - Tenable.sc plugin logs Tenable.sc vulnerability logs Tenable.sc asset logs" + }, + { + "title": "Cisco IOS", + "id": "cisco_ios", + "description": "Collect logs from Cisco IOS with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_ios.log-*", + "title": "Cisco IOS logs" + } + ], + "elser_embedding": "Cisco IOS - Collect logs from Cisco IOS with Elastic Agent. - Cisco IOS logs" + }, + { + "title": "ZooKeeper Metrics", + "id": "zookeeper", + "description": "Collect metrics from ZooKeeper service with Elastic Agent.", + "data_streams": [ + { + "dataset": "connection", + "index_pattern": "logs-zookeeper.connection-*", + "title": "ZooKeeper connection metrics" + }, + { + "dataset": "mntr", + "index_pattern": "logs-zookeeper.mntr-*", + "title": "ZooKeeper mntr metrics" + }, + { + "dataset": "server", + "index_pattern": "logs-zookeeper.server-*", + "title": "ZooKeeper server metrics" + } + ], + "elser_embedding": "ZooKeeper Metrics - Collect metrics from ZooKeeper service with Elastic Agent. - ZooKeeper connection metrics ZooKeeper mntr metrics ZooKeeper server metrics" + }, + { + "title": "Palo Alto Next-Gen Firewall", + "id": "panw", + "description": "Collect logs from Palo Alto next-gen firewalls with Elastic Agent.", + "data_streams": [ + { + "dataset": "panos", + "index_pattern": "logs-panw.panos-*", + "title": "Palo Alto Networks PAN-OS firewall logs" + } + ], + "elser_embedding": "Palo Alto Next-Gen Firewall - Collect logs from Palo Alto next-gen firewalls with Elastic Agent. - Palo Alto Networks PAN-OS firewall logs" + }, + { + "title": "Hadoop", + "id": "hadoop", + "description": "Collect metrics from Apache Hadoop with Elastic Agent.", + "data_streams": [ + { + "dataset": "namenode", + "index_pattern": "logs-hadoop.namenode-*", + "title": "NameNode Metrics" + }, + { + "dataset": "datanode", + "index_pattern": "logs-hadoop.datanode-*", + "title": "DataNode metrics" + }, + { + "dataset": "node_manager", + "index_pattern": "logs-hadoop.node_manager-*", + "title": "Node Manager metrics" + }, + { + "dataset": "application", + "index_pattern": "logs-hadoop.application-*", + "title": "Application metrics" + }, + { + "dataset": "cluster", + "index_pattern": "logs-hadoop.cluster-*", + "title": "Cluster metrics" + } + ], + "elser_embedding": "Hadoop - Collect metrics from Apache Hadoop with Elastic Agent. - NameNode Metrics DataNode metrics Node Manager metrics Application metrics Cluster metrics" + }, + { + "title": "InfluxDb", + "id": "influxdb", + "description": "Collect metrics from Influxdb database", + "data_streams": [ + { + "dataset": "advstatus", + "index_pattern": "logs-influxdb.advstatus-*", + "title": "InfluxDB database advanced status metrics" + }, + { + "dataset": "status", + "index_pattern": "logs-influxdb.status-*", + "title": "InfluxDB database status metrics" + } + ], + "elser_embedding": "InfluxDb - Collect metrics from Influxdb database - InfluxDB database advanced status metrics InfluxDB database status metrics" + }, + { + "title": "Sophos", + "id": "sophos", + "description": "Collect logs from Sophos with Elastic Agent.", + "data_streams": [ + { + "dataset": "xg", + "index_pattern": "logs-sophos.xg-*", + "title": "Sophos XG logs" + }, + { + "dataset": "utm", + "index_pattern": "logs-sophos.utm-*", + "title": "Sophos UTM logs" + } + ], + "elser_embedding": "Sophos - Collect logs from Sophos with Elastic Agent. - Sophos XG logs Sophos UTM logs" + }, + { + "title": "Menlo Security", + "id": "menlo", + "description": "Collect logs from Menlo Security products with Elastic Agent", + "data_streams": [ + { + "dataset": "dlp", + "index_pattern": "logs-menlo.dlp-*", + "title": "Collect Menlo DLP from Menlo Security API" + }, + { + "dataset": "web", + "index_pattern": "logs-menlo.web-*", + "title": "Collect Menlo Web from Menlo Security API" + } + ], + "elser_embedding": "Menlo Security - Collect logs from Menlo Security products with Elastic Agent - Collect Menlo DLP from Menlo Security API Collect Menlo Web from Menlo Security API" + }, + { + "title": "Barracuda Web Application Firewall", + "id": "barracuda", + "description": "Collect logs from Barracuda Web Application Firewall with Elastic Agent.", + "data_streams": [ + { + "dataset": "waf", + "index_pattern": "logs-barracuda.waf-*", + "title": "Barracuda WAF Logs" + } + ], + "elser_embedding": "Barracuda Web Application Firewall - Collect logs from Barracuda Web Application Firewall with Elastic Agent. - Barracuda WAF Logs" + }, + { + "title": "FireEye Network Security", + "id": "fireeye", + "description": "Collect logs from FireEye NX with Elastic Agent.", + "data_streams": [ + { + "dataset": "nx", + "index_pattern": "logs-fireeye.nx-*", + "title": "Fireeye NX" + } + ], + "elser_embedding": "FireEye Network Security - Collect logs from FireEye NX with Elastic Agent. - Fireeye NX" + }, + { + "title": "Tines", + "id": "tines", + "description": "Tines Logs & Time Saved Reports", + "data_streams": [ + { + "dataset": "time_saved", + "index_pattern": "logs-tines.time_saved-*", + "title": "Tines Time Saved Reports" + }, + { + "dataset": "audit_logs", + "index_pattern": "logs-tines.audit_logs-*", + "title": "Tines Audit Logs" + } + ], + "elser_embedding": "Tines - Tines Logs & Time Saved Reports - Tines Time Saved Reports Tines Audit Logs" + }, + { + "title": "Cisco Meraki Metrics", + "id": "cisco_meraki_metrics", + "description": "Collect metrics from Cisco Meraki with Elastic Agent.", + "data_streams": [ + { + "dataset": "device_health", + "index_pattern": "logs-cisco_meraki_metrics.device_health-*", + "title": "Cisco Meraki Device Health Metrics" + } + ], + "elser_embedding": "Cisco Meraki Metrics - Collect metrics from Cisco Meraki with Elastic Agent. - Cisco Meraki Device Health Metrics" + }, + { + "title": "VMware vSphere", + "id": "vsphere", + "description": "This Elastic integration collects metrics and logs from vSphere/vCenter servers", + "data_streams": [ + { + "dataset": "network", + "index_pattern": "logs-vsphere.network-*", + "title": "vSphere network metrics" + }, + { + "dataset": "resourcepool", + "index_pattern": "logs-vsphere.resourcepool-*", + "title": "vSphere resourcepool metrics" + }, + { + "dataset": "datastore", + "index_pattern": "logs-vsphere.datastore-*", + "title": "vSphere datastore metrics" + }, + { + "dataset": "virtualmachine", + "index_pattern": "logs-vsphere.virtualmachine-*", + "title": "vSphere virtual machine metrics" + }, + { + "dataset": "host", + "index_pattern": "logs-vsphere.host-*", + "title": "vSphere host metrics" + }, + { + "dataset": "datastorecluster", + "index_pattern": "logs-vsphere.datastorecluster-*", + "title": "vSphere DatastoreCluster metrics" + }, + { + "dataset": "log", + "index_pattern": "logs-vsphere.log-*", + "title": "vSphere Logs" + }, + { + "dataset": "cluster", + "index_pattern": "logs-vsphere.cluster-*", + "title": "vSphere cluster metrics" + } + ], + "elser_embedding": "VMware vSphere - This Elastic integration collects metrics and logs from vSphere/vCenter servers - vSphere network metrics vSphere resourcepool metrics vSphere datastore metrics vSphere virtual machine metrics vSphere host metrics vSphere DatastoreCluster metrics vSphere Logs vSphere cluster metrics" + }, + { + "title": "Platform Observability", + "id": "platform_observability", + "description": "Collect stack component logs with Elastic Agent", + "data_streams": [ + { + "dataset": "kibana_audit", + "index_pattern": "logs-platform_observability.kibana_audit-*", + "title": "Platform Observability Kibana audit logs" + }, + { + "dataset": "kibana_log", + "index_pattern": "logs-platform_observability.kibana_log-*", + "title": "Platform Observability Kibana logs" + } + ], + "elser_embedding": "Platform Observability - Collect stack component logs with Elastic Agent - Platform Observability Kibana audit logs Platform Observability Kibana logs" + }, + { + "title": "System", + "id": "system", + "description": "Collect system logs and metrics from your servers with Elastic Agent.", + "data_streams": [ + { + "dataset": "memory", + "index_pattern": "logs-system.memory-*", + "title": "System memory metrics" + }, + { + "dataset": "network", + "index_pattern": "logs-system.network-*", + "title": "System network metrics" + }, + { + "dataset": "uptime", + "index_pattern": "logs-system.uptime-*", + "title": "System uptime metrics" + }, + { + "dataset": "socket_summary", + "index_pattern": "logs-system.socket_summary-*", + "title": "System socket_summary metrics" + }, + { + "dataset": "auth", + "index_pattern": "logs-system.auth-*", + "title": "System auth logs" + }, + { + "dataset": "process", + "index_pattern": "logs-system.process-*", + "title": "System process metrics" + }, + { + "dataset": "load", + "index_pattern": "logs-system.load-*", + "title": "System load metrics" + }, + { + "dataset": "application", + "index_pattern": "logs-system.application-*", + "title": "Windows Application Events" + }, + { + "dataset": "diskio", + "index_pattern": "logs-system.diskio-*", + "title": "System diskio metrics" + }, + { + "dataset": "syslog", + "index_pattern": "logs-system.syslog-*", + "title": "System syslog logs" + }, + { + "dataset": "filesystem", + "index_pattern": "logs-system.filesystem-*", + "title": "System filesystem metrics" + }, + { + "dataset": "fsstat", + "index_pattern": "logs-system.fsstat-*", + "title": "System fsstat metrics" + }, + { + "dataset": "core", + "index_pattern": "logs-system.core-*", + "title": "System core metrics" + }, + { + "dataset": "cpu", + "index_pattern": "logs-system.cpu-*", + "title": "System cpu metrics" + }, + { + "dataset": "process_summary", + "index_pattern": "logs-system.process_summary-*", + "title": "System process_summary metrics" + }, + { + "dataset": "system", + "index_pattern": "logs-system.system-*", + "title": "Windows System Events" + }, + { + "dataset": "security", + "index_pattern": "logs-system.security-*", + "title": "Security logs" + } + ], + "elser_embedding": "System - Collect system logs and metrics from your servers with Elastic Agent. - System memory metrics System network metrics System uptime metrics System socket_summary metrics System auth logs System process metrics System load metrics Windows Application Events System diskio metrics System syslog logs System filesystem metrics System fsstat metrics System core metrics System cpu metrics System process_summary metrics Windows System Events Security logs" + }, + { + "title": "Airflow", + "id": "airflow", + "description": "Airflow Integration.", + "data_streams": [ + { + "dataset": "statsd", + "index_pattern": "logs-airflow.statsd-*", + "title": "Airflow metrics" + } + ], + "elser_embedding": "Airflow - Airflow Integration. - Airflow metrics" + }, + { + "title": "Custom Google Pub/Sub Logs", + "id": "gcp_pubsub", + "description": "Collect Logs from Google Pub/Sub topics", + "data_streams": [], + "elser_embedding": "Custom Google Pub/Sub Logs - Collect Logs from Google Pub/Sub topics - " + }, + { + "title": "Beat", + "id": "beat", + "description": "Beat Integration", + "data_streams": [], + "elser_embedding": "Beat - Beat Integration - " + }, + { + "title": "Cyberark Privileged Threat Analytics", + "id": "cyberark_pta", + "description": "Collect security logs from Cyberark PTA integration.", + "data_streams": [ + { + "dataset": "events", + "index_pattern": "logs-cyberark_pta.events-*", + "title": "CyberArk PTA logs" + } + ], + "elser_embedding": "Cyberark Privileged Threat Analytics - Collect security logs from Cyberark PTA integration. - CyberArk PTA logs" + }, + { + "title": "Trellix ePO Cloud", + "id": "trellix_epo_cloud", + "description": "Collect logs from Trellix ePO Cloud with Elastic Agent.", + "data_streams": [ + { + "dataset": "device", + "index_pattern": "logs-trellix_epo_cloud.device-*", + "title": "Collect Device logs from Trellix ePO Cloud." + }, + { + "dataset": "group", + "index_pattern": "logs-trellix_epo_cloud.group-*", + "title": "Collect Group logs from Trellix ePO Cloud." + }, + { + "dataset": "event", + "index_pattern": "logs-trellix_epo_cloud.event-*", + "title": "Collect Event logs from Trellix ePO Cloud." + } + ], + "elser_embedding": "Trellix ePO Cloud - Collect logs from Trellix ePO Cloud with Elastic Agent. - Collect Device logs from Trellix ePO Cloud. Collect Group logs from Trellix ePO Cloud. Collect Event logs from Trellix ePO Cloud." + }, + { + "title": "Vectra Detect", + "id": "vectra_detect", + "description": "Collect logs from Vectra Detect with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-vectra_detect.log-*", + "title": "Collect logs from Vectra Detect" + } + ], + "elser_embedding": "Vectra Detect - Collect logs from Vectra Detect with Elastic Agent. - Collect logs from Vectra Detect" + }, + { + "title": "Atlassian Confluence", + "id": "atlassian_confluence", + "description": "Collect logs from Atlassian Confluence with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-atlassian_confluence.audit-*", + "title": "Confluence Audit Logs" + } + ], + "elser_embedding": "Atlassian Confluence - Collect logs from Atlassian Confluence with Elastic Agent. - Confluence Audit Logs" + }, + { + "title": "QNAP NAS", + "id": "qnap_nas", + "description": "Collect logs from QNAP NAS devices with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-qnap_nas.log-*", + "title": "QNAP NAS logs" + } + ], + "elser_embedding": "QNAP NAS - Collect logs from QNAP NAS devices with Elastic Agent. - QNAP NAS logs" + }, + { + "title": "Memcached", + "id": "memcached", + "description": "Memcached Integration", + "data_streams": [ + { + "dataset": "stats", + "index_pattern": "logs-memcached.stats-*", + "title": "Memcached stats metrics" + } + ], + "elser_embedding": "Memcached - Memcached Integration - Memcached stats metrics" + }, + { + "title": "Azure Resource Metrics", + "id": "azure_metrics", + "description": "Collect metrics from Azure resources with Elastic Agent.", + "data_streams": [ + { + "dataset": "container_service", + "index_pattern": "logs-azure_metrics.container_service-*", + "title": "Container Service" + }, + { + "dataset": "container_instance", + "index_pattern": "logs-azure_metrics.container_instance-*", + "title": "Container Instance" + }, + { + "dataset": "compute_vm", + "index_pattern": "logs-azure_metrics.compute_vm-*", + "title": "Compute VM" + }, + { + "dataset": "monitor", + "index_pattern": "logs-azure_metrics.monitor-*", + "title": "Monitor" + }, + { + "dataset": "storage_account", + "index_pattern": "logs-azure_metrics.storage_account-*", + "title": "Storage Account" + }, + { + "dataset": "compute_vm_scaleset", + "index_pattern": "logs-azure_metrics.compute_vm_scaleset-*", + "title": "Compute VM Scaleset" + }, + { + "dataset": "database_account", + "index_pattern": "logs-azure_metrics.database_account-*", + "title": "Database Account" + }, + { + "dataset": "container_registry", + "index_pattern": "logs-azure_metrics.container_registry-*", + "title": "Container Registry" + } + ], + "elser_embedding": "Azure Resource Metrics - Collect metrics from Azure resources with Elastic Agent. - Container Service Container Instance Compute VM Monitor Storage Account Compute VM Scaleset Database Account Container Registry" + }, + { + "title": "Elastic Connectors", + "id": "elastic_connectors", + "description": "Sync data from source to the Elasticsearch index.", + "data_streams": [], + "elser_embedding": "Elastic Connectors - Sync data from source to the Elasticsearch index. - " + }, + { + "title": "StatsD Input", + "id": "statsd_input", + "description": "StatsD Input Package", + "data_streams": [], + "elser_embedding": "StatsD Input - StatsD Input Package - " + }, + { + "title": "Cloudflare", + "id": "cloudflare", + "description": "Collect logs from Cloudflare with Elastic Agent.", + "data_streams": [ + { + "dataset": "logpull", + "index_pattern": "logs-cloudflare.logpull-*", + "title": "Cloudflare Logpull" + }, + { + "dataset": "audit", + "index_pattern": "logs-cloudflare.audit-*", + "title": "Cloudflare Audit Logs" + } + ], + "elser_embedding": "Cloudflare - Collect logs from Cloudflare with Elastic Agent. - Cloudflare Logpull Cloudflare Audit Logs" + }, + { + "title": "Cribl", + "id": "cribl", + "description": "Stream logs from Cribl into Elastic.", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-cribl.logs-*", + "title": "Logs" + } + ], + "elser_embedding": "Cribl - Stream logs from Cribl into Elastic. - Logs" + }, + { + "title": "PHP-FPM", + "id": "php_fpm", + "description": "This Elastic integration collects metrics from PHP-FPM.", + "data_streams": [ + { + "dataset": "process", + "index_pattern": "logs-php_fpm.process-*", + "title": "Process metrics" + }, + { + "dataset": "pool", + "index_pattern": "logs-php_fpm.pool-*", + "title": "Pool metrics" + } + ], + "elser_embedding": "PHP-FPM - This Elastic integration collects metrics from PHP-FPM. - Process metrics Pool metrics" + }, + { + "title": "Azure Logs", + "id": "azure", + "description": "This Elastic integration collects logs from Azure", + "data_streams": [ + { + "dataset": "platformlogs", + "index_pattern": "logs-azure.platformlogs-*", + "title": "Azure Platform Logs" + }, + { + "dataset": "auditlogs", + "index_pattern": "logs-azure.auditlogs-*", + "title": "Azure Audit Logs" + }, + { + "dataset": "springcloudlogs", + "index_pattern": "logs-azure.springcloudlogs-*", + "title": "Azure Spring Apps Logs" + }, + { + "dataset": "signinlogs", + "index_pattern": "logs-azure.signinlogs-*", + "title": "Azure Signin Logs" + }, + { + "dataset": "firewall_logs", + "index_pattern": "logs-azure.firewall_logs-*", + "title": "Collect Network rule logs from Azure Firewall" + }, + { + "dataset": "graphactivitylogs", + "index_pattern": "logs-azure.graphactivitylogs-*", + "title": "Microsoft Graph Activity Logs" + }, + { + "dataset": "application_gateway", + "index_pattern": "logs-azure.application_gateway-*", + "title": "Azure Application Gateway logs" + }, + { + "dataset": "eventhub", + "index_pattern": "logs-azure.eventhub-*", + "title": "Azure Event Hub Input" + }, + { + "dataset": "provisioning", + "index_pattern": "logs-azure.provisioning-*", + "title": "Microsoft Entra ID Provisioning Logs" + }, + { + "dataset": "activitylogs", + "index_pattern": "logs-azure.activitylogs-*", + "title": "Azure Activity Logs" + }, + { + "dataset": "identity_protection", + "index_pattern": "logs-azure.identity_protection-*", + "title": "Microsoft Entra ID Identity Protection Logs" + } + ], + "elser_embedding": "Azure Logs - This Elastic integration collects logs from Azure - Azure Platform Logs Azure Audit Logs Azure Spring Apps Logs Azure Signin Logs Collect Network rule logs from Azure Firewall Microsoft Graph Activity Logs Azure Application Gateway logs Azure Event Hub Input Microsoft Entra ID Provisioning Logs Azure Activity Logs Microsoft Entra ID Identity Protection Logs" + }, + { + "title": "Palo Alto Networks Metrics", + "id": "panw_metrics", + "description": "Collect metrics from Palo Alto Networks with Elastic Agent.", + "data_streams": [ + { + "dataset": "vpn", + "index_pattern": "logs-panw_metrics.vpn-*", + "title": "Palo Alto Networks VPN metrics" + }, + { + "dataset": "interfaces", + "index_pattern": "logs-panw_metrics.interfaces-*", + "title": "Palo Alto Networks Interfaces metrics" + }, + { + "dataset": "system", + "index_pattern": "logs-panw_metrics.system-*", + "title": "Palo Alto Networks System metrics" + }, + { + "dataset": "routing", + "index_pattern": "logs-panw_metrics.routing-*", + "title": "Palo Alto Networks Routing metrics" + } + ], + "elser_embedding": "Palo Alto Networks Metrics - Collect metrics from Palo Alto Networks with Elastic Agent. - Palo Alto Networks VPN metrics Palo Alto Networks Interfaces metrics Palo Alto Networks System metrics Palo Alto Networks Routing metrics" + }, + { + "title": "Custom Threat Intelligence", + "id": "ti_custom", + "description": "Ingest threat intelligence data in STIX 2.1 format with Elastic Agent", + "data_streams": [ + { + "dataset": "indicator", + "index_pattern": "logs-ti_custom.indicator-*", + "title": "STIX 2.1 indicators" + } + ], + "elser_embedding": "Custom Threat Intelligence - Ingest threat intelligence data in STIX 2.1 format with Elastic Agent - STIX 2.1 indicators" + }, + { + "title": "Check Point Harmony Endpoint", + "id": "checkpoint_harmony_endpoint", + "description": "Collect logs from Check Point Harmony Endpoint", + "data_streams": [ + { + "dataset": "urlfiltering", + "index_pattern": "logs-checkpoint_harmony_endpoint.urlfiltering-*", + "title": "URL Filtering" + }, + { + "dataset": "forensics", + "index_pattern": "logs-checkpoint_harmony_endpoint.forensics-*", + "title": "Forensics" + }, + { + "dataset": "antibot", + "index_pattern": "logs-checkpoint_harmony_endpoint.antibot-*", + "title": "Anti-Bot" + }, + { + "dataset": "threatemulation", + "index_pattern": "logs-checkpoint_harmony_endpoint.threatemulation-*", + "title": "Threat Emulation" + }, + { + "dataset": "threatextraction", + "index_pattern": "logs-checkpoint_harmony_endpoint.threatextraction-*", + "title": "Threat Extraction" + }, + { + "dataset": "zerophishing", + "index_pattern": "logs-checkpoint_harmony_endpoint.zerophishing-*", + "title": "Zero Phishing" + }, + { + "dataset": "antimalware", + "index_pattern": "logs-checkpoint_harmony_endpoint.antimalware-*", + "title": "Anti-Malware" + } + ], + "elser_embedding": "Check Point Harmony Endpoint - Collect logs from Check Point Harmony Endpoint - URL Filtering Forensics Anti-Bot Threat Emulation Threat Extraction Zero Phishing Anti-Malware" + }, + { + "title": "Thycotic Secret Server", + "id": "thycotic_ss", + "description": "Thycotic Secret Server logs", + "data_streams": [ + { + "dataset": "logs", + "index_pattern": "logs-thycotic_ss.logs-*", + "title": "Thycotic Secret Server Logs" + } + ], + "elser_embedding": "Thycotic Secret Server - Thycotic Secret Server logs - Thycotic Secret Server Logs" + }, + { + "title": "Custom HTTP Endpoint Logs", + "id": "http_endpoint", + "description": "Collect JSON data from listening HTTP port with Elastic Agent.", + "data_streams": [], + "elser_embedding": "Custom HTTP Endpoint Logs - Collect JSON data from listening HTTP port with Elastic Agent. - " + }, + { + "title": "Atlassian Bitbucket", + "id": "atlassian_bitbucket", + "description": "Collect logs from Atlassian Bitbucket with Elastic Agent.", + "data_streams": [ + { + "dataset": "audit", + "index_pattern": "logs-atlassian_bitbucket.audit-*", + "title": "Bitbucket Audit Logs" + } + ], + "elser_embedding": "Atlassian Bitbucket - Collect logs from Atlassian Bitbucket with Elastic Agent. - Bitbucket Audit Logs" + }, + { + "title": "TYCHON Agentless", + "id": "tychon", + "description": "Collect complete master endpoint datasets including vulnerability and STIG to comply with DISA endpoint requirements and C2C without adding services to your endpoints.", + "data_streams": [ + { + "dataset": "systemcerts", + "index_pattern": "logs-tychon.systemcerts-*", + "title": "System Certificates" + }, + { + "dataset": "stig", + "index_pattern": "logs-tychon.stig-*", + "title": "Endpoint STIG Results" + }, + { + "dataset": "softwareinventory", + "index_pattern": "logs-tychon.softwareinventory-*", + "title": "Endpoint Software Inventory Info" + }, + { + "dataset": "coams", + "index_pattern": "logs-tychon.coams-*", + "title": "Endpoint Operational Attributes (Requires DATT)" + }, + { + "dataset": "harddrive", + "index_pattern": "logs-tychon.harddrive-*", + "title": "Endpoint Harddrive Info" + }, + { + "dataset": "host", + "index_pattern": "logs-tychon.host-*", + "title": "Host Operating System Info" + }, + { + "dataset": "cve", + "index_pattern": "logs-tychon.cve-*", + "title": "Vulnerabilites" + }, + { + "dataset": "externaldevicecontrol", + "index_pattern": "logs-tychon.externaldevicecontrol-*", + "title": "Endpoint External Device Control" + }, + { + "dataset": "cmrs", + "index_pattern": "logs-tychon.cmrs-*", + "title": "DISA Continuous Monitoring and Risk Scoring Data" + }, + { + "dataset": "arp", + "index_pattern": "logs-tychon.arp-*", + "title": "Endpoint Arp Table Information" + }, + { + "dataset": "ciphers", + "index_pattern": "logs-tychon.ciphers-*", + "title": "Certificate Ciphers" + }, + { + "dataset": "features", + "index_pattern": "logs-tychon.features-*", + "title": "Features Info" + }, + { + "dataset": "epp", + "index_pattern": "logs-tychon.epp-*", + "title": "Endpoint Protection Platform Info" + }, + { + "dataset": "cpu", + "index_pattern": "logs-tychon.cpu-*", + "title": "Endpoint CPU Info" + }, + { + "dataset": "browser", + "index_pattern": "logs-tychon.browser-*", + "title": "Endpoint Browser Configurations" + }, + { + "dataset": "exposedservice", + "index_pattern": "logs-tychon.exposedservice-*", + "title": "Endpoint Exposed Services" + }, + { + "dataset": "volume", + "index_pattern": "logs-tychon.volume-*", + "title": "Endpoint Volumes Info" + }, + { + "dataset": "hardware", + "index_pattern": "logs-tychon.hardware-*", + "title": "Hardware Info" + }, + { + "dataset": "networkadapter", + "index_pattern": "logs-tychon.networkadapter-*", + "title": "Network Adapters" + } + ], + "elser_embedding": "TYCHON Agentless - Collect complete master endpoint datasets including vulnerability and STIG to comply with DISA endpoint requirements and C2C without adding services to your endpoints. - System Certificates Endpoint STIG Results Endpoint Software Inventory Info Endpoint Operational Attributes (Requires DATT) Endpoint Harddrive Info Host Operating System Info Vulnerabilites Endpoint External Device Control DISA Continuous Monitoring and Risk Scoring Data Endpoint Arp Table Information Certificate Ciphers Features Info Endpoint Protection Platform Info Endpoint CPU Info Endpoint Browser Configurations Endpoint Exposed Services Endpoint Volumes Info Hardware Info Network Adapters" + }, + { + "title": "Proofpoint On Demand", + "id": "proofpoint_on_demand", + "description": "Collect logs from Proofpoint On Demand with Elastic Agent.", + "data_streams": [ + { + "dataset": "message", + "index_pattern": "logs-proofpoint_on_demand.message-*", + "title": "Proofpoint On Demand Message logs" + }, + { + "dataset": "audit", + "index_pattern": "logs-proofpoint_on_demand.audit-*", + "title": "Proofpoint On Demand Audit logs" + }, + { + "dataset": "mail", + "index_pattern": "logs-proofpoint_on_demand.mail-*", + "title": "Proofpoint On Demand Mail logs" + } + ], + "elser_embedding": "Proofpoint On Demand - Collect logs from Proofpoint On Demand with Elastic Agent. - Proofpoint On Demand Message logs Proofpoint On Demand Audit logs Proofpoint On Demand Mail logs" + }, + { + "title": "Cisco ASA", + "id": "cisco_asa", + "description": "Collect logs from Cisco ASA with Elastic Agent.", + "data_streams": [ + { + "dataset": "log", + "index_pattern": "logs-cisco_asa.log-*", + "title": "Cisco ASA logs" + } + ], + "elser_embedding": "Cisco ASA - Collect logs from Cisco ASA with Elastic Agent. - Cisco ASA logs" + }, + { + "title": "Amazon Security Lake", + "id": "amazon_security_lake", + "description": "Collect logs from Amazon Security Lake with Elastic Agent.", + "data_streams": [ + { + "dataset": "network_activity", + "index_pattern": "logs-amazon_security_lake.network_activity-*", + "title": "Amazon Security Lake Network Activity Events" + }, + { + "dataset": "application_activity", + "index_pattern": "logs-amazon_security_lake.application_activity-*", + "title": "Amazon Security Lake Application Activity Events" + }, + { + "dataset": "discovery", + "index_pattern": "logs-amazon_security_lake.discovery-*", + "title": "Amazon Security Lake Discovery Events" + }, + { + "dataset": "findings", + "index_pattern": "logs-amazon_security_lake.findings-*", + "title": "Amazon Security Lake Findings Events" + }, + { + "dataset": "system_activity", + "index_pattern": "logs-amazon_security_lake.system_activity-*", + "title": "Amazon Security Lake System Activity Events" + }, + { + "dataset": "event", + "index_pattern": "logs-amazon_security_lake.event-*", + "title": "Collect Amazon Security Lake Events" + }, + { + "dataset": "iam", + "index_pattern": "logs-amazon_security_lake.iam-*", + "title": "Amazon Security Lake Identity and Access Management Events" + } + ], + "elser_embedding": "Amazon Security Lake - Collect logs from Amazon Security Lake with Elastic Agent. - Amazon Security Lake Network Activity Events Amazon Security Lake Application Activity Events Amazon Security Lake Discovery Events Amazon Security Lake Findings Events Amazon Security Lake System Activity Events Collect Amazon Security Lake Events Amazon Security Lake Identity and Access Management Events" + }, + { + "title": "Azure App Service", + "id": "azure_app_service", + "description": "Collect logs from Azure App Service with Elastic Agent.", + "data_streams": [ + { + "dataset": "app_service_logs", + "index_pattern": "logs-azure_app_service.app_service_logs-*", + "title": "Collect App Service logs from Azure" + } + ], + "elser_embedding": "Azure App Service - Collect logs from Azure App Service with Elastic Agent. - Collect App Service logs from Azure" + }, + { + "title": "ESET PROTECT", + "id": "eset_protect", + "description": "Collect logs from ESET PROTECT with Elastic Agent.", + "data_streams": [ + { + "dataset": "device_task", + "index_pattern": "logs-eset_protect.device_task-*", + "title": "Collect Device Task logs from ESET PROTECT" + }, + { + "dataset": "detection", + "index_pattern": "logs-eset_protect.detection-*", + "title": "Collect Detection logs from ESET PROTECT" + }, + { + "dataset": "event", + "index_pattern": "logs-eset_protect.event-*", + "title": "Collect Event logs from ESET PROTECT" + } + ], + "elser_embedding": "ESET PROTECT - Collect logs from ESET PROTECT with Elastic Agent. - Collect Device Task logs from ESET PROTECT Collect Detection logs from ESET PROTECT Collect Event logs from ESET PROTECT" + }, + { + "id": "endpoint", + "title": "Elastic Defend", + "description": "windows linux osx dns network process suspicious user registry host host-based endpoint analysis commandline cli command exfiltration ransomware detection system os operating traffic prevention file user modification integrity obfuscation powershell anomaly edr xdr", + "data_streams": [ + { + "dataset": "endpoint.events.api", + "title": "Endpoint API Events", + "index_pattern": "logs-endpoint.events.api-*" + }, + { + "dataset": "endpoint.events.file", + "title": "Endpoint File Events", + "index_pattern": "logs-endpoint.events.file-*" + }, + { + "dataset": "endpoint.events.library", + "title": "Endpoint Library and Driver Events", + "index_pattern": "logs-endpoint.events.library-*" + }, + { + "dataset": "endpoint.events.network", + "title": "Endpoint Network Events", + "index_pattern": "logs-endpoint.events.network-*" + }, + { + "dataset": "endpoint.events.process", + "title": "Endpoint Process Events", + "index_pattern": "logs-endpoint.events.process-*" + }, + { + "dataset": "endpoint.events.registry", + "title": "Endpoint Registry Events", + "index_pattern": "logs-endpoint.events.registry-*" + }, + { + "dataset": "endpoint.events.security", + "title": "Endpoint Security Events", + "index_pattern": "logs-endpoint.events.security-*" + } + ], + "elser_embedding": "Elastic Defend - windows linux osx dns network process suspicious user registry host host-based endpoint analysis commandline cli command exfiltration ransomware detection system os operating traffic prevention file user modification integrity obfuscation powershell anomaly edr xdr - Endpoint API Events Endpoint File Events Endpoint Library and Driver Events Endpoint Network Events Endpoint Process Events Endpoint Registry Events Endpoint Security Events" + } +] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts index 4f0b65e063b77..14825326eee0e 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts @@ -5,9 +5,9 @@ * 2.0. */ +import type { SearchHit, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import assert from 'assert'; -import type { SearchHit, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { Stored } from '../types'; import type { IndexNameProvider } from './rule_migrations_data_client'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts index 40f4aa6bf786e..8960edd0cce21 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts @@ -6,8 +6,9 @@ */ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client'; +import { RuleMigrationsDataIntegrationsClient } from './rule_migrations_data_integrations_client'; import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client'; +import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client'; import type { AdapterId } from './rule_migrations_data_service'; export type IndexNameProvider = () => Promise<string>; @@ -16,6 +17,7 @@ export type IndexNameProviders = Record<AdapterId, IndexNameProvider>; export class RuleMigrationsDataClient { public readonly rules: RuleMigrationsDataRulesClient; public readonly resources: RuleMigrationsDataResourcesClient; + public readonly integrations: RuleMigrationsDataIntegrationsClient; constructor( indexNameProviders: IndexNameProviders, @@ -35,5 +37,11 @@ export class RuleMigrationsDataClient { esClient, logger ); + this.integrations = new RuleMigrationsDataIntegrationsClient( + indexNameProviders.integrations, + username, + esClient, + logger + ); } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.ts new file mode 100644 index 0000000000000..3fdf1d11de36c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_integrations_client.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 type { Integration } from '../types'; +import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; + +/* This will be removed once the package registry changes is performed */ +import integrationsFile from './integrations_temp.json'; + +/* The minimum score required for a integration to be considered correct, might need to change this later */ +const MIN_SCORE = 40 as const; +/* The number of integrations the RAG will return, sorted by score */ +const RETURNED_INTEGRATIONS = 5 as const; + +/* This is a temp implementation to allow further development until https://github.com/elastic/package-registry/issues/1252 */ +const INTEGRATIONS = integrationsFile as Integration[]; +/* BULK_MAX_SIZE defines the number to break down the bulk operations by. + * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. + */ +export class RuleMigrationsDataIntegrationsClient extends RuleMigrationsDataBaseClient { + /** Indexes an array of integrations to be used with ELSER semantic search queries */ + async create(): Promise<void> { + const index = await this.getIndexName(); + await this.esClient + .bulk({ + refresh: 'wait_for', + operations: INTEGRATIONS.flatMap((integration) => [ + { update: { _index: index, _id: integration.id } }, + { + doc: { + title: integration.title, + description: integration.description, + data_streams: integration.data_streams, + elser_embedding: integration.elser_embedding, + '@timestamp': new Date().toISOString(), + }, + doc_as_upsert: true, + }, + ]), + }) + .catch((error) => { + this.logger.error(`Error indexing integration details for ELSER: ${error.message}`); + throw error; + }); + } + + /** Based on a LLM generated semantic string, returns the 5 best results with a score above 40 */ + async retrieveIntegrations(semanticString: string): Promise<Integration[]> { + const index = await this.getIndexName(); + const query = { + bool: { + should: [ + { + semantic: { + query: semanticString, + field: 'elser_embedding', + boost: 1.5, + }, + }, + { + multi_match: { + query: semanticString, + fields: ['title^2', 'description'], + boost: 3, + }, + }, + ], + }, + }; + const results = await this.esClient + .search<Integration>({ + index, + query, + size: RETURNED_INTEGRATIONS, + min_score: MIN_SCORE, + }) + .then(this.processResponseHits.bind(this)) + .catch((error) => { + this.logger.error(`Error querying integration details for ELSER: ${error.message}`); + throw error; + }); + + return results; + } +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index a01d36e9a1195..06e257b9862c5 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -6,8 +6,10 @@ */ import type { + AggregationsAggregationContainer, AggregationsFilterAggregate, AggregationsMaxAggregate, + AggregationsMinAggregate, AggregationsStringTermsAggregate, AggregationsStringTermsBucket, QueryDslQueryContainer, @@ -30,7 +32,7 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial<ElasticRule> } & 'id' | 'translation_result' | 'comments' >; export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>; -export type RuleMigrationAllDataStats = Array<RuleMigrationDataStats & { migration_id: string }>; +export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. @@ -217,6 +219,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + createdAt: { min: { field: '@timestamp' } }, lastUpdatedAt: { max: { field: 'updated_at' } }, }; const result = await this.esClient @@ -226,30 +229,33 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient throw error; }); - const { pending, processing, completed, lastUpdatedAt, failed } = result.aggregations ?? {}; + const bucket = result.aggregations ?? {}; return { + id: migrationId, rules: { total: this.getTotalHits(result), - pending: (pending as AggregationsFilterAggregate)?.doc_count ?? 0, - processing: (processing as AggregationsFilterAggregate)?.doc_count ?? 0, - completed: (completed as AggregationsFilterAggregate)?.doc_count ?? 0, - failed: (failed as AggregationsFilterAggregate)?.doc_count ?? 0, + pending: (bucket.pending as AggregationsFilterAggregate)?.doc_count ?? 0, + processing: (bucket.processing as AggregationsFilterAggregate)?.doc_count ?? 0, + completed: (bucket.completed as AggregationsFilterAggregate)?.doc_count ?? 0, + failed: (bucket.failed as AggregationsFilterAggregate)?.doc_count ?? 0, }, - last_updated_at: (lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string, + created_at: (bucket.createdAt as AggregationsMinAggregate)?.value_as_string ?? '', + last_updated_at: (bucket.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '', }; } - /** Retrieves the stats for all the rule migrations aggregated by migration id */ + /** Retrieves the stats for all the rule migrations aggregated by migration id, in creation order */ async getAllStats(): Promise<RuleMigrationAllDataStats> { const index = await this.getIndexName(); - const aggregations = { + const aggregations: { migrationIds: AggregationsAggregationContainer } = { migrationIds: { - terms: { field: 'migration_id' }, + terms: { field: 'migration_id', order: { createdAt: 'asc' } }, aggregations: { pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + createdAt: { min: { field: '@timestamp' } }, lastUpdatedAt: { max: { field: 'updated_at' } }, }, }, @@ -264,7 +270,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate; const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? []; return buckets.map((bucket) => ({ - migration_id: bucket.key, + id: bucket.key, rules: { total: bucket.doc_count, pending: bucket.pending?.doc_count ?? 0, @@ -272,6 +278,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient completed: bucket.completed?.doc_count ?? 0, failed: bucket.failed?.doc_count ?? 0, }, + created_at: bucket.createdAt?.value_as_string, last_updated_at: bucket.lastUpdatedAt?.value_as_string, })); } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts index e738bd85e2f1a..f8cc0c3f1c076 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { INDEX_PATTERN, RuleMigrationsDataService } from './rule_migrations_data_service'; -import { Subject } from 'rxjs'; -import type { InstallParams } from '@kbn/index-adapter'; -import { IndexPatternAdapter } from '@kbn/index-adapter'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { loggerMock } from '@kbn/logging-mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { securityServiceMock } from '@kbn/core-security-server-mocks'; +import type { InstallParams } from '@kbn/index-adapter'; +import { IndexPatternAdapter } from '@kbn/index-adapter'; +import { loggerMock } from '@kbn/logging-mocks'; +import { Subject } from 'rxjs'; import type { IndexNameProviders } from './rule_migrations_data_client'; +import { INDEX_PATTERN, RuleMigrationsDataService } from './rule_migrations_data_service'; jest.mock('@kbn/index-adapter'); @@ -42,7 +42,7 @@ describe('SiemRuleMigrationsDataService', () => { describe('constructor', () => { it('should create IndexPatternAdapters', () => { new RuleMigrationsDataService(logger, kibanaVersion); - expect(MockedIndexPatternAdapter).toHaveBeenCalledTimes(2); + expect(MockedIndexPatternAdapter).toHaveBeenCalledTimes(3); }); it('should create component templates', () => { @@ -54,6 +54,9 @@ describe('SiemRuleMigrationsDataService', () => { expect(indexPatternAdapter.setComponentTemplate).toHaveBeenCalledWith( expect.objectContaining({ name: `${INDEX_PATTERN}-resources` }) ); + expect(indexPatternAdapter.setComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ name: `${INDEX_PATTERN}-integrations` }) + ); }); it('should create index templates', () => { @@ -65,6 +68,9 @@ describe('SiemRuleMigrationsDataService', () => { expect(indexPatternAdapter.setIndexTemplate).toHaveBeenCalledWith( expect.objectContaining({ name: `${INDEX_PATTERN}-resources` }) ); + expect(indexPatternAdapter.setIndexTemplate).toHaveBeenCalledWith( + expect.objectContaining({ name: `${INDEX_PATTERN}-integrations` }) + ); }); }); @@ -92,8 +98,11 @@ describe('SiemRuleMigrationsDataService', () => { logger: loggerMock.create(), pluginStop$: new Subject(), }; - const [rulesIndexPatternAdapter, resourcesIndexPatternAdapter] = - MockedIndexPatternAdapter.mock.instances; + const [ + rulesIndexPatternAdapter, + resourcesIndexPatternAdapter, + integrationsIndexPatternAdapter, + ] = MockedIndexPatternAdapter.mock.instances; (rulesIndexPatternAdapter.install as jest.Mock).mockResolvedValueOnce(undefined); await index.install(params); @@ -101,12 +110,16 @@ describe('SiemRuleMigrationsDataService', () => { await mockIndexNameProviders.rules(); await mockIndexNameProviders.resources(); + await mockIndexNameProviders.integrations(); expect(rulesIndexPatternAdapter.createIndex).toHaveBeenCalledWith('space1'); expect(rulesIndexPatternAdapter.getIndexName).toHaveBeenCalledWith('space1'); expect(resourcesIndexPatternAdapter.createIndex).toHaveBeenCalledWith('space1'); expect(resourcesIndexPatternAdapter.getIndexName).toHaveBeenCalledWith('space1'); + + expect(integrationsIndexPatternAdapter.createIndex).toHaveBeenCalledWith('space1'); + expect(integrationsIndexPatternAdapter.getIndexName).toHaveBeenCalledWith('space1'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts index c19a89cefd81a..ceff8e05f9f2f 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts @@ -6,17 +6,18 @@ */ import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { IndexPatternAdapter, type FieldMap, type InstallParams } from '@kbn/index-adapter'; +import type { IndexNameProvider, IndexNameProviders } from './rule_migrations_data_client'; +import { RuleMigrationsDataClient } from './rule_migrations_data_client'; import { - ruleMigrationsFieldMap, + integrationsFieldMap, ruleMigrationResourcesFieldMap, + ruleMigrationsFieldMap, } from './rule_migrations_field_maps'; -import type { IndexNameProvider, IndexNameProviders } from './rule_migrations_data_client'; -import { RuleMigrationsDataClient } from './rule_migrations_data_client'; const TOTAL_FIELDS_LIMIT = 2500; export const INDEX_PATTERN = '.kibana-siem-rule-migrations'; -export type AdapterId = 'rules' | 'resources'; +export type AdapterId = 'rules' | 'resources' | 'integrations'; interface CreateClientParams { spaceId: string; @@ -31,6 +32,7 @@ export class RuleMigrationsDataService { this.adapters = { rules: this.createAdapter({ id: 'rules', fieldMap: ruleMigrationsFieldMap }), resources: this.createAdapter({ id: 'resources', fieldMap: ruleMigrationResourcesFieldMap }), + integrations: this.createAdapter({ id: 'integrations', fieldMap: integrationsFieldMap }), }; } @@ -49,6 +51,7 @@ export class RuleMigrationsDataService { await Promise.all([ this.adapters.rules.install({ ...params, logger: this.logger }), this.adapters.resources.install({ ...params, logger: this.logger }), + this.adapters.integrations.install({ ...params, logger: this.logger }), ]); } @@ -56,6 +59,7 @@ export class RuleMigrationsDataService { const indexNameProviders: IndexNameProviders = { rules: this.createIndexNameProvider('rules', spaceId), resources: this.createIndexNameProvider('resources', spaceId), + integrations: this.createIndexNameProvider('integrations', spaceId), }; return new RuleMigrationsDataClient( diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 3811ff74b5ca1..8e8a3c5ee0f27 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -26,6 +26,7 @@ export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigrat 'original_rule.mitre_attack_ids': { type: 'keyword', array: true, required: false }, elastic_rule: { type: 'nested', required: false }, 'elastic_rule.title': { type: 'keyword', required: true }, + 'elastic_rule.integration_ids': { type: 'keyword', array: true, required: false }, 'elastic_rule.query': { type: 'text', required: true }, 'elastic_rule.query_language': { type: 'keyword', required: true }, 'elastic_rule.description': { type: 'keyword', required: false }, @@ -49,3 +50,14 @@ export const ruleMigrationResourcesFieldMap: FieldMap< updated_at: { type: 'date', required: false }, updated_by: { type: 'keyword', required: false }, }; + +export const integrationsFieldMap: FieldMap = { + '@timestamp': { type: 'date', required: true }, + title: { type: 'text', required: true }, + description: { type: 'text', required: true }, + data_streams: { type: 'nested', array: true, required: true }, + 'data_streams.dataset': { type: 'keyword', required: true }, + 'data_streams.title': { type: 'text', required: true }, + 'data_streams.index_pattern': { type: 'keyword', required: true }, + elser_embeddings: { type: 'semantic_text', required: true }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts index 0db4bbe18feeb..0ec705a9268dc 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts @@ -6,24 +6,26 @@ */ import { END, START, StateGraph } from '@langchain/langgraph'; +import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule'; import { migrateRuleState } from './state'; +import { getTranslateRuleGraph } from './sub_graphs/translate_rule'; import type { MigrateRuleGraphParams, MigrateRuleState } from './types'; -import { getTranslateQueryNode } from './nodes/translate_query'; -import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule'; export function getRuleMigrationAgent({ model, inferenceClient, prebuiltRulesMap, resourceRetriever, + integrationRetriever, connectorId, logger, }: MigrateRuleGraphParams) { const matchPrebuiltRuleNode = getMatchPrebuiltRuleNode({ model, prebuiltRulesMap, logger }); - const translationNode = getTranslateQueryNode({ + const translationSubGraph = getTranslateRuleGraph({ model, inferenceClient, resourceRetriever, + integrationRetriever, connectorId, logger, }); @@ -31,7 +33,7 @@ export function getRuleMigrationAgent({ const translateRuleGraph = new StateGraph(migrateRuleState) // Nodes .addNode('matchPrebuiltRule', matchPrebuiltRuleNode) - .addNode('translation', translationNode) + .addNode('translation', translationSubGraph) // Edges .addEdge(START, 'matchPrebuiltRule') .addConditionalEdges('matchPrebuiltRule', matchedPrebuiltRuleConditional) diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.ts deleted file mode 100644 index 45b7a36dd292d..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/replace_resources_prompt.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 type { RuleMigrationResources } from '../../../../util/rule_resource_retriever'; -import type { MigrateRuleState } from '../../../types'; - -const getResourcesContext = (resources: RuleMigrationResources): string => { - const resourcesContext = []; - if (resources.macro?.length) { - const macrosSummary = resources.macro - .map((macro) => `\`${macro.name}\`: ${macro.content}`) - .join('\n'); - resourcesContext.push('<<MACROS>>', macrosSummary, '<</MACROS>>'); - } - if (resources.list?.length) { - const lookupsSummary = resources.list - .map((list) => `lookup ${list.name}: ${list.content}`) - .join('\n'); - resourcesContext.push('<<LOOKUP_TABLES>>', lookupsSummary, '<</LOOKUP_TABLES>>'); - } - return resourcesContext.join('\n'); -}; - -export const getReplaceQueryResourcesPrompt = ( - state: MigrateRuleState, - resources: RuleMigrationResources -): string => { - const resourcesContext = getResourcesContext(resources); - return `You are an agent expert in Splunk SPL (Search Processing Language). -Your task is to inline a set of macros and lookup table values in a SPL query. - -## Guidelines: - -- You will be provided with a SPL query and also the resources reference with the values of macros and lookup tables. -- You have to replace the macros and lookup tables in the SPL query with their actual values. -- The original and modified queries must be equivalent. -- Macros names have the number of arguments in parentheses, e.g., \`macroName(2)\`. You must replace the correct macro accounting for the number of arguments. - -## Process: - -- Go through the SPL query and identify all the macros and lookup tables that are used. Two scenarios may arise: - - The macro or lookup table is provided in the resources: Replace the call by their actual value in the query. - - The macro or lookup table is not provided in the resources: Keep the call in the query as it is. - -## Example: - Having the following macros: - \`someSource\`: sourcetype="somesource" - \`searchTitle(1)\`: search title="$value$" - \`searchTitle\`: search title=* - \`searchType\`: search type=* - And the following SPL query: - \`\`\`spl - \`someSource\` \`someFilter\` - | \`searchTitle("sometitle")\` - | \`searchType("sometype")\` - | table * - \`\`\` - The correct replacement would be: - \`\`\`spl - sourcetype="somesource" \`someFilter\` - | search title="sometitle" - | \`searchType("sometype")\` - | table * - \`\`\` - -## Important: You must respond only with the modified query inside a \`\`\`spl code block, nothing else. - -# Find the macros and lookup tables below: - -${resourcesContext} - -# Find the SPL query below: - -\`\`\`spl -${state.original_rule.query} -\`\`\` - -`; -}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts new file mode 100644 index 0000000000000..f986098e9deb0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.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 { END, START, StateGraph } from '@langchain/langgraph'; +import { getProcessQueryNode } from './nodes/process_query'; +import { getRetrieveIntegrationsNode } from './nodes/retrieve_integrations'; +import { getTranslateRuleNode } from './nodes/translate_rule'; +import { translateRuleState } from './state'; +import type { TranslateRuleGraphParams } from './types'; + +export function getTranslateRuleGraph({ + model, + inferenceClient, + resourceRetriever, + integrationRetriever, + connectorId, + logger, +}: TranslateRuleGraphParams) { + const translateRuleNode = getTranslateRuleNode({ + model, + inferenceClient, + resourceRetriever, + connectorId, + logger, + }); + const processQueryNode = getProcessQueryNode({ + model, + resourceRetriever, + }); + const retrieveIntegrationsNode = getRetrieveIntegrationsNode({ + model, + integrationRetriever, + }); + + const translateRuleGraph = new StateGraph(translateRuleState) + // Nodes + .addNode('processQuery', processQueryNode) + .addNode('retrieveIntegrations', retrieveIntegrationsNode) + .addNode('translateRule', translateRuleNode) + // Edges + .addEdge(START, 'processQuery') + .addEdge('processQuery', 'retrieveIntegrations') + .addEdge('retrieveIntegrations', 'translateRule') + .addEdge('translateRule', END); + + const graph = translateRuleGraph.compile(); + graph.name = 'Translate Rule Graph'; + return graph; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/index.ts similarity index 81% rename from x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts rename to x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/index.ts index 7d247f755e9da..6a2b7e9cebd7e 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/index.ts @@ -4,4 +4,5 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export { getTranslateQueryNode } from './translate_query'; + +export { getTranslateRuleGraph } from './graph'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/index.ts new file mode 100644 index 0000000000000..6feb852eba474 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getProcessQueryNode } from './process_query'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts new file mode 100644 index 0000000000000..0f90d74dafba3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.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 { StringOutputParser } from '@langchain/core/output_parsers'; +import { isEmpty } from 'lodash/fp'; +import type { ChatModel } from '../../../../../util/actions_client_chat'; +import type { RuleResourceRetriever } from '../../../../../util/rule_resource_retriever'; +import type { GraphNode } from '../../types'; +import { getReplaceQueryResourcesPrompt } from './prompts'; + +interface GetProcessQueryNodeParams { + model: ChatModel; + resourceRetriever: RuleResourceRetriever; +} + +export const getProcessQueryNode = ({ + model, + resourceRetriever, +}: GetProcessQueryNodeParams): GraphNode => { + return async (state) => { + let query = state.original_rule.query; + const resources = await resourceRetriever.getResources(state.original_rule); + if (!isEmpty(resources)) { + const replaceQueryResourcesPrompt = getReplaceQueryResourcesPrompt(state, resources); + const stringParser = new StringOutputParser(); + query = await model.pipe(stringParser).invoke(replaceQueryResourcesPrompt); + } + return { inline_query: query }; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts new file mode 100644 index 0000000000000..5d2e6648c1d85 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts @@ -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 type { RuleMigrationResources } from '../../../../../util/rule_resource_retriever'; +import type { TranslateRuleState } from '../../types'; + +const getResourcesContext = (resources: RuleMigrationResources): string => { + const resourcesContext = []; + if (resources.macro?.length) { + const macrosSummary = resources.macro + .map((macro) => `\`${macro.name}\`: ${macro.content}`) + .join('\n'); + resourcesContext.push('<<MACROS>>', macrosSummary, '<</MACROS>>'); + } + if (resources.list?.length) { + const lookupsSummary = resources.list + .map((list) => `lookup ${list.name}: ${list.content}`) + .join('\n'); + resourcesContext.push('<<LOOKUP_TABLES>>', lookupsSummary, '<</LOOKUP_TABLES>>'); + } + return resourcesContext.join('\n'); +}; + +export const getReplaceQueryResourcesPrompt = ( + state: TranslateRuleState, + resources: RuleMigrationResources +): string => { + const resourcesContext = getResourcesContext(resources); + return `You are an agent expert in Splunk SPL (Search Processing Language). +Your task is to inline a set of macros and lookup tables syntax using their values in a SPL query. + +# Guidelines +- You will be provided with a SPL query and also the resources reference with the values of macros and lookup tables. +- You have to replace the macros and lookup tables syntax in the SPL query and use their values inline, if provided. +- The original and modified queries must be equivalent. + +# Process +- Go through the SPL query and identify all the macros and lookup tables that are used. Two scenarios may arise: + - The macro or lookup table is provided in the resources: Replace it using its actual content. + - The macro or lookup table is not provided in the resources: Do not replace it, keep it in the query as it is. + +## Macros replacements + +### Notes: +- Macros names have the number of arguments in parentheses, e.g., \`macroName(2)\`. You must replace the correct macro accounting for the number of arguments. + +### Example: + Having the following macros: + \`someSource\`: sourcetype="somesource" + \`searchTitle(1)\`: search title="$value$" + \`searchTitle\`: search title=* + \`searchType\`: search type=* + And the following SPL query: + \`\`\`spl + \`someSource\` \`someFilter\` + | \`searchTitle("sometitle")\` + | \`searchType("sometype")\` + | table * + \`\`\` + The correct replacement would be: + \`\`\`spl + sourcetype="somesource" \`someFilter\` + | search title="sometitle" + | \`searchType("sometype")\` + | table * + \`\`\` + +## Lookups replacements + +### Notes: +- OUTPUTNEW and OUTPUT fields should be replaced with the values from the lookup table. +- Use the \`case\` function to evaluate conditions in the same order provided by the lookup table. +- Ensure all lookup matching fields are correctly matched to their respective case conditions. +- If there are more than one field to match, use the \`AND\` operator to combine them inside the \`case\` function. +- The transformed SPL query should function equivalently to the original query with the \`lookup\` command. + +### Example: + Having the following lookup table: + uid,username,department + 1066,Claudia Garcia,Engineering + 1690,Rutherford Sullivan,Engineering + 1815,Vanya Patel,IT + 1862,Wei Zhang,Engineering + 1916,Alex Martin,Personnel + And the following SPL query: + \`\`\`spl + ... | lookup users uid OUTPUTNEW username, department + \`\`\` + The correct replacement would be: + \`\`\`spl + ... | eval username=case(uid=1066, "Claudia Garcia", + uid=1690, "Rutherford Sullivan", + uid=1815, "Vanya Patel", + uid=1862, "Wei Zhang", + uid=1916, "Alex Martin", + true, null), + department=case(uid=1066, "Engineering", + uid=1690, "Engineering", + uid=1815, "IT", + uid=1862, "Engineering", + uid=1916, "Personnel", + true, null) + \`\`\` + + +## Important: You must respond only with the modified query inside a \`\`\`spl code block, nothing else. + +# Find the macros and lookup tables below: + +${resourcesContext} + +# Find the SPL query below: + +\`\`\`spl +${state.original_rule.query} +\`\`\` + +`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/index.ts new file mode 100644 index 0000000000000..7db89035c6ad7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getRetrieveIntegrationsNode } from './retrieve_integrations'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/prompts.ts new file mode 100644 index 0000000000000..4d15ad71d6794 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/prompts.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChatPromptTemplate } from '@langchain/core/prompts'; + +export const CREATE_SEMANTIC_QUERY_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful assistant that helps in translating provided titles, descriptions and data sources into a single summary of keywords specifically crafted to be used as a semantic search query, which are usually short and includes keywords that are valid for the usecase. + The data provided are collected from SIEM detection rules, and it is trying to match the description of a list of data sources, so provide good keywords that match this usecase. + Try to also detect what sort of vendor, solution or technology is required and add these as keywords as well. + Some examples would be to identify if its cloud, which vendor, network, host, endpoint, etc.`, + ], + [ + 'human', + `<query> +Title: {title} +Description: {description} +Query: {query} +</query> + +Go through the relevant title, description and data sources from the above query and create a collection of keywords specifically crafted to be used as a semantic search query. + +<guidelines> +- The query should be short and concise. +- Include keywords that are relevant to the use case. +- Add related keywords you detected from the above query, like one or more vendor, product, cloud provider, OS platform etc. +- Always reply with a JSON object with the key "query" and the value as the semantic search query inside three backticks as shown in the below example. +</guidelines> + +<example> +U: <query> +Title: Processes created by netsh +Description: This search looks for processes launching netsh.exe to execute various commands via the netsh command-line utility. Netsh.exe is a command-line scripting utility that allows you to, either locally or remotely, display or modify the network configuration of a computer that is currently running. Netsh can be used as a persistence proxy technique to execute a helper .dll when netsh.exe is executed. In this search, we are looking for processes spawned by netsh.exe that are executing commands via the command line. Deprecated because we have another detection of the same type. +Data Sources: +</query> +A: Please find the query keywords JSON object below: +\`\`\`json +{{"query": "windows host endpoint netsh.exe process creation command-line utility network configuration persistence proxy dll execution sysmon event id 1"}} +\`\`\` +</example>`, + ], + ['ai', 'Please find the query keywords JSON object below:'], +]); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/retrieve_integrations.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/retrieve_integrations.ts new file mode 100644 index 0000000000000..18577532fdf66 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/retrieve_integrations/retrieve_integrations.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 { JsonOutputParser } from '@langchain/core/output_parsers'; +import type { ChatModel } from '../../../../../util/actions_client_chat'; +import type { IntegrationRetriever } from '../../../../../util/integration_retriever'; +import type { GraphNode } from '../../types'; +import { CREATE_SEMANTIC_QUERY_PROMPT } from './prompts'; + +interface GetRetrieveIntegrationsNodeParams { + model: ChatModel; + integrationRetriever: IntegrationRetriever; +} + +interface GetSemanticQueryResponse { + query: string; +} + +export const getRetrieveIntegrationsNode = ({ + model, + integrationRetriever, +}: GetRetrieveIntegrationsNodeParams): GraphNode => { + const jsonParser = new JsonOutputParser(); + const semanticQueryChain = CREATE_SEMANTIC_QUERY_PROMPT.pipe(model).pipe(jsonParser); + + return async (state) => { + const query = state.inline_query; + + const integrationQuery = (await semanticQueryChain.invoke({ + title: state.original_rule.title, + description: state.original_rule.description, + query, + })) as GetSemanticQueryResponse; + + const integrations = await integrationRetriever.getIntegrations(integrationQuery.query); + return { + integrations, + }; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/esql_knowledge_base_caller.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts rename to x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/esql_knowledge_base_caller.ts diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/index.ts new file mode 100644 index 0000000000000..c8c5678b7f2f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getTranslateRuleNode } from './translate_rule'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/esql_translation_prompt.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts similarity index 85% rename from x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/esql_translation_prompt.ts rename to x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts index 05e3c5c63bbe3..3e77353bba8b1 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompts/esql_translation_prompt.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts @@ -5,9 +5,12 @@ * 2.0. */ -import type { MigrateRuleState } from '../../../types'; +import type { TranslateRuleState } from '../../types'; -export const getEsqlTranslationPrompt = (state: MigrateRuleState, query: string): string => { +export const getEsqlTranslationPrompt = ( + state: TranslateRuleState, + indexPatterns: string +): string => { return `You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk to Elastic Security. Your goal is to translate the SPL query into an equivalent Elastic Security Query Language (ES|QL) query. @@ -19,7 +22,7 @@ Your goal is to translate the SPL query into an equivalent Elastic Security Quer ## Guidelines: - Analyze the SPL query and identify the key components. - Translate the SPL query into an equivalent ES|QL query using ECS (Elastic Common Schema) field names. -- Always use logs* index pattern for the ES|QL translated query. +- Always start the generated ES|QL query by filtering FROM using these index patterns in the translated query: ${indexPatterns}. - If, in the SPL query, you find a lookup list or macro call, mention it in the summary and add a placeholder in the query with the format [macro:<macro_name>(argumentCount)] or [lookup:<lookup_name>] including the [] keys, - Examples: - \`get_duration(firstDate,secondDate)\` -> [macro:get_duration(2)] @@ -40,7 +43,7 @@ ${state.original_rule.description} <</DESCRIPTION>> <<SPL_QUERY>> -${query} +${state.inline_query} <</SPL_QUERY>> `; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts similarity index 61% rename from x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts rename to x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index e12d3b96ceb3f..3fcd968b55650 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts @@ -7,17 +7,14 @@ import type { Logger } from '@kbn/core/server'; import type { InferenceClient } from '@kbn/inference-plugin/server'; -import { StringOutputParser } from '@langchain/core/output_parsers'; -import { isEmpty } from 'lodash/fp'; +import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; +import type { ChatModel } from '../../../../../util/actions_client_chat'; +import type { RuleResourceRetriever } from '../../../../../util/rule_resource_retriever'; import type { GraphNode } from '../../types'; import { getEsqlKnowledgeBase } from './esql_knowledge_base_caller'; -import { getReplaceQueryResourcesPrompt } from './prompts/replace_resources_prompt'; -import { getEsqlTranslationPrompt } from './prompts/esql_translation_prompt'; -import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; -import type { RuleResourceRetriever } from '../../../util/rule_resource_retriever'; -import type { ChatModel } from '../../../util/actions_client_chat'; +import { getEsqlTranslationPrompt } from './prompts'; -interface GetTranslateQueryNodeParams { +interface GetTranslateRuleNodeParams { model: ChatModel; inferenceClient: InferenceClient; resourceRetriever: RuleResourceRetriever; @@ -25,25 +22,19 @@ interface GetTranslateQueryNodeParams { logger: Logger; } -export const getTranslateQueryNode = ({ - model, +export const getTranslateRuleNode = ({ inferenceClient, - resourceRetriever, connectorId, logger, -}: GetTranslateQueryNodeParams): GraphNode => { +}: GetTranslateRuleNodeParams): GraphNode => { const esqlKnowledgeBaseCaller = getEsqlKnowledgeBase({ inferenceClient, connectorId, logger }); return async (state) => { - let query = state.original_rule.query; + const indexPatterns = state.integrations.flatMap((integration) => + integration.data_streams.map((dataStream) => dataStream.index_pattern) + ); + const integrationIds = state.integrations.map((integration) => integration.id); - const resources = await resourceRetriever.getResources(state.original_rule); - if (!isEmpty(resources)) { - const replaceQueryResourcesPrompt = getReplaceQueryResourcesPrompt(state, resources); - const stringParser = new StringOutputParser(); - query = await model.pipe(stringParser).invoke(replaceQueryResourcesPrompt); - } - - const prompt = getEsqlTranslationPrompt(state, query); + const prompt = getEsqlTranslationPrompt(state, indexPatterns.join(' ')); const response = await esqlKnowledgeBaseCaller(prompt); const esqlQuery = response.match(/```esql\n([\s\S]*?)\n```/)?.[1] ?? ''; @@ -57,6 +48,7 @@ export const getTranslateQueryNode = ({ translation_result: translationResult, elastic_rule: { title: state.original_rule.title, + integration_ids: integrationIds, description: state.original_rule.description, severity: 'low', query: esqlQuery, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts new file mode 100644 index 0000000000000..8c8e9780aedf8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { BaseMessage } from '@langchain/core/messages'; +import { Annotation, messagesStateReducer } from '@langchain/langgraph'; +import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; +import type { + ElasticRule, + OriginalRule, + RuleMigration, +} from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { Integration } from '../../../../types'; + +export const translateRuleState = Annotation.Root({ + messages: Annotation<BaseMessage[]>({ + reducer: messagesStateReducer, + default: () => [], + }), + original_rule: Annotation<OriginalRule>(), + integrations: Annotation<Integration[]>({ + reducer: (current, value) => value ?? current, + default: () => [], + }), + inline_query: Annotation<string>({ + reducer: (current, value) => value ?? current, + default: () => '', + }), + elastic_rule: Annotation<ElasticRule>({ + reducer: (state, action) => ({ ...state, ...action }), + default: () => ({} as ElasticRule), + }), + translation_result: Annotation<SiemMigrationRuleTranslationResult>({ + reducer: (current, value) => value ?? current, + default: () => SiemMigrationRuleTranslationResult.UNTRANSLATABLE, + }), + comments: Annotation<RuleMigration['comments']>({ + reducer: (current, value) => (value ? (current ?? []).concat(value) : current), + default: () => [], + }), + response: Annotation<string>({ + reducer: (current, value) => value ?? current, + default: () => '', + }), +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts new file mode 100644 index 0000000000000..42bf8e14c5924 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { ChatModel } from '../../../util/actions_client_chat'; +import type { IntegrationRetriever } from '../../../util/integration_retriever'; +import type { RuleResourceRetriever } from '../../../util/rule_resource_retriever'; +import type { translateRuleState } from './state'; + +export type TranslateRuleState = typeof translateRuleState.State; +export type GraphNode = (state: TranslateRuleState) => Promise<Partial<TranslateRuleState>>; + +export interface TranslateRuleGraphParams { + inferenceClient: InferenceClient; + model: ChatModel; + connectorId: string; + resourceRetriever: RuleResourceRetriever; + integrationRetriever: IntegrationRetriever; + logger: Logger; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts index 975c03439842e..046083140e5e5 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts @@ -7,10 +7,11 @@ import type { Logger } from '@kbn/core/server'; import type { InferenceClient } from '@kbn/inference-plugin/server'; -import type { migrateRuleState } from './state'; import type { ChatModel } from '../util/actions_client_chat'; +import type { IntegrationRetriever } from '../util/integration_retriever'; import type { PrebuiltRulesMapByName } from '../util/prebuilt_rules'; import type { RuleResourceRetriever } from '../util/rule_resource_retriever'; +import type { migrateRuleState } from './state'; export type MigrateRuleState = typeof migrateRuleState.State; export type GraphNode = (state: MigrateRuleState) => Promise<Partial<MigrateRuleState>>; @@ -21,5 +22,6 @@ export interface MigrateRuleGraphParams { connectorId: string; prebuiltRulesMap: PrebuiltRulesMapByName; resourceRetriever: RuleResourceRetriever; + integrationRetriever: IntegrationRetriever; logger: Logger; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 989c33a44cb36..56c7e8485d315 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -9,24 +9,22 @@ import type { AuthenticatedUser, Logger } from '@kbn/core/server'; import { AbortError, abortSignalToPromise } from '@kbn/kibana-utils-plugin/server'; import type { RunnableConfig } from '@langchain/core/runnables'; import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; -import type { - RuleMigrationAllTaskStats, - RuleMigrationTaskStats, -} from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationsDataClient } from '../data/rule_migrations_data_client'; import type { RuleMigrationDataStats } from '../data/rule_migrations_data_rules_client'; +import { getRuleMigrationAgent } from './agent'; +import type { MigrateRuleState } from './agent/types'; import type { + MigrationAgent, + RuleMigrationTaskPrepareParams, + RuleMigrationTaskRunParams, RuleMigrationTaskStartParams, RuleMigrationTaskStartResult, RuleMigrationTaskStopResult, - RuleMigrationTaskPrepareParams, - RuleMigrationTaskRunParams, - MigrationAgent, } from './types'; -import { getRuleMigrationAgent } from './agent'; -import type { MigrateRuleState } from './agent/types'; -import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; import { ActionsClientChat } from './util/actions_client_chat'; +import { IntegrationRetriever } from './util/integration_retriever'; +import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; import { RuleResourceRetriever } from './util/rule_resource_retriever'; const ITERATION_BATCH_SIZE = 50 as const; @@ -89,6 +87,7 @@ export class RuleMigrationsTaskClient { }: RuleMigrationTaskPrepareParams): Promise<MigrationAgent> { const prebuiltRulesMap = await retrievePrebuiltRulesMap({ soClient, rulesClient }); const resourceRetriever = new RuleResourceRetriever(migrationId, this.data); + const integrationRetriever = new IntegrationRetriever(this.data); const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); const model = await actionsClientChat.createModel({ @@ -102,6 +101,7 @@ export class RuleMigrationsTaskClient { inferenceClient, prebuiltRulesMap, resourceRetriever, + integrationRetriever, logger: this.logger, }); return agent; @@ -226,10 +226,10 @@ export class RuleMigrationsTaskClient { } /** Returns the stats of all migrations */ - async getAllStats(): Promise<RuleMigrationAllTaskStats> { + async getAllStats(): Promise<RuleMigrationTaskStats[]> { const allDataStats = await this.data.rules.getAllStats(); return allDataStats.map((dataStats) => { - const status = this.getTaskStatus(dataStats.migration_id, dataStats.rules); + const status = this.getTaskStatus(dataStats.id, dataStats.rules); return { status, ...dataStats }; }); } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.test.ts new file mode 100644 index 0000000000000..2aa01a9c9c41d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRuleMigrationsDataClient } from '../../data/__mocks__/mocks'; +import { IntegrationRetriever } from './integration_retriever'; + +describe('IntegrationRetriever', () => { + let integrationRetriever: IntegrationRetriever; + const mockRuleMigrationsDataClient = new MockRuleMigrationsDataClient(); + const mockIntegrationItem = { + id: '1', + title: 'Integration 1', + description: 'Integration 1 description', + data_streams: [{ dataset: 'test', title: 'dstitle', index_pattern: 'logs-*' }], + elser_embedding: 'elser_embedding', + }; + beforeEach(() => { + integrationRetriever = new IntegrationRetriever(mockRuleMigrationsDataClient); + mockRuleMigrationsDataClient.integrations.retrieveIntegrations.mockImplementation( + async (_: string) => { + return mockIntegrationItem; + } + ); + }); + + it('should retrieve integrations', async () => { + const result = await integrationRetriever.getIntegrations('test'); + + expect(mockRuleMigrationsDataClient.integrations.retrieveIntegrations).toHaveBeenCalledWith( + 'test' + ); + expect(result).toEqual(mockIntegrationItem); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.ts new file mode 100644 index 0000000000000..7913e2c438081 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/integration_retriever.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; +import type { Integration } from '../../types'; + +export class IntegrationRetriever { + constructor(private readonly dataClient: RuleMigrationsDataClient) {} + + public async getIntegrations(semanticString: string): Promise<Integration[]> { + return this.integrationRetriever(semanticString); + } + + private integrationRetriever = async (semanticString: string): Promise<Integration[]> => { + const integrations = await this.dataClient.integrations.retrieveIntegrations(semanticString); + + return integrations; + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts index e506b43cc323b..f8a0f0b3b25a7 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts @@ -14,3 +14,11 @@ export type Stored<T extends object> = T & { id: string }; export type StoredRuleMigration = Stored<RuleMigration>; export type StoredRuleMigrationResource = Stored<RuleMigrationResource>; + +export interface Integration { + title: string; + id: string; + description: string; + data_streams: Array<{ dataset: string; title: string; index_pattern: string }>; + elser_embedding: string; +} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index e2ec9d0e1b535..98bbf3a80777f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -56,7 +56,11 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerPolicyRoutes } from './endpoint/routes/policy'; import { registerActionRoutes } from './endpoint/routes/actions'; import { registerEndpointSuggestionsRoutes } from './endpoint/routes/suggestions'; -import { EndpointArtifactClient, ManifestManager } from './endpoint/services'; +import { + EndpointArtifactClient, + ManifestManager, + securityWorkflowInsightsService, +} from './endpoint/services'; import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; import type { EndpointAppContext } from './endpoint/types'; import { initUsageCollectors } from './usage'; @@ -519,6 +523,12 @@ export class Plugin implements ISecuritySolutionPlugin { featureUsageService.setup(plugins.licensing); + securityWorkflowInsightsService.setup({ + kibanaVersion: pluginContext.env.packageInfo.version, + logger: this.logger, + isFeatureEnabled: config.experimentalFeatures.defendInsights, + }); + return { setProductFeaturesConfigurator: productFeaturesService.setProductFeaturesConfigurator.bind(productFeaturesService), @@ -672,6 +682,12 @@ export class Plugin implements ISecuritySolutionPlugin { this.telemetryReceiver ); + securityWorkflowInsightsService + .start({ + esClient: core.elasticsearch.client.asInternalUser, + }) + .catch(() => {}); + const endpointPkgInstallationPromise = this.endpointContext.service .getInternalFleetServices() .packages.getInstallation(FLEET_ENDPOINT_PACKAGE); @@ -727,6 +743,7 @@ export class Plugin implements ISecuritySolutionPlugin { this.policyWatcher?.stop(); this.completeExternalResponseActionsTask.stop().catch(() => {}); this.siemMigrationsService.stop(); + securityWorkflowInsightsService.stop(); licenseService.stop(); } } diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts index f44ad77e67929..d80be5f4a421b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts @@ -7,9 +7,7 @@ import { mapValues, isObject, isArray } from 'lodash/fp'; import { set } from '@kbn/safer-lodash-set'; - -import { toArray } from '../../../common/utils/to_array'; -import { isGeoField } from '../../../common/utils/field_formatters'; +import { toArray, isGeoField } from '@kbn/timelines-plugin/common'; export const mapObjectValuesToStringArray = (object: object): object => mapValues((o) => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts index f40edfc5914df..baed5b3c35605 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts @@ -6,7 +6,7 @@ */ import { set } from '@kbn/safer-lodash-set'; import { get, isEmpty } from 'lodash/fp'; -import { toObjectArrayOfStrings } from '../../../common/utils/to_array'; +import { toObjectArrayOfStrings } from '@kbn/timelines-plugin/common'; export function getFlattenedFields<T>( fields: string[], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 8707f10ed01cb..61ab1c5bca583 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -8,12 +8,12 @@ import { set } from '@kbn/safer-lodash-set/fp'; import { get, has } from 'lodash/fp'; import { hostFieldsMap } from '@kbn/securitysolution-ecs'; +import { toObjectArrayOfStrings } from '@kbn/timelines-plugin/common'; import type { HostAggEsItem, HostsEdges, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 94d45b2b63e0e..cc1a084f6b5a7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -13,6 +13,7 @@ import type { SavedObjectsClientContract, } from '@kbn/core/server'; import { hostFieldsMap } from '@kbn/securitysolution-ecs'; +import { toObjectArrayOfStrings } from '@kbn/timelines-plugin/common'; import { Direction } from '../../../../../../common/search_strategy/common'; import type { AggregationRequest, @@ -22,7 +23,6 @@ import type { HostItem, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import type { EndpointAppContext } from '../../../../../endpoint/types'; import { getPendingActionsSummary } from '../../../../../endpoint/services'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts index 06452c915009c..b92b67f244ebe 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts @@ -8,7 +8,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@kbn/safer-lodash-set/fp'; import { sourceFieldsMap, hostFieldsMap } from '@kbn/securitysolution-ecs'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; +import { toObjectArrayOfStrings } from '@kbn/timelines-plugin/common'; import type { AuthenticationsEdges, AuthenticationHit, diff --git a/x-pack/plugins/serverless_observability/kibana.jsonc b/x-pack/plugins/serverless_observability/kibana.jsonc index fce943c44865a..ad2c1f76ce563 100644 --- a/x-pack/plugins/serverless_observability/kibana.jsonc +++ b/x-pack/plugins/serverless_observability/kibana.jsonc @@ -25,7 +25,9 @@ "discover", "security" ], - "optionalPlugins": [], + "optionalPlugins": [ + "streams" + ], "requiredBundles": [] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/serverless_observability/public/navigation_tree.ts b/x-pack/plugins/serverless_observability/public/navigation_tree.ts index 7501a75abe876..e6fb3c9107306 100644 --- a/x-pack/plugins/serverless_observability/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_observability/public/navigation_tree.ts @@ -8,374 +8,387 @@ import { i18n } from '@kbn/i18n'; import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; -export const navigationTree: NavigationTreeDefinition = { - body: [ - { type: 'recentlyAccessed' }, - { - type: 'navGroup', - id: 'observability_project_nav', - title: 'Observability', - icon: 'logoObservability', - defaultIsCollapsed: false, - isCollapsible: false, - breadcrumbStatus: 'hidden', - children: [ - { - title: i18n.translate('xpack.serverlessObservability.nav.discover', { - defaultMessage: 'Discover', - }), - link: 'last-used-logs-viewer', - // avoid duplicate "Discover" breadcrumbs - breadcrumbStatus: 'hidden', - renderAs: 'item', - children: [ - { - link: 'discover', - children: [ - { - link: 'observability-logs-explorer', - }, - ], - }, - ], - }, - { - title: i18n.translate('xpack.serverlessObservability.nav.dashboards', { - defaultMessage: 'Dashboards', - }), - link: 'dashboards', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dashboards')); +export const createNavigationTree = ({ + streamsAvailable, +}: { + streamsAvailable?: boolean; +}): NavigationTreeDefinition => { + return { + body: [ + { type: 'recentlyAccessed' }, + { + type: 'navGroup', + id: 'observability_project_nav', + title: 'Observability', + icon: 'logoObservability', + defaultIsCollapsed: false, + isCollapsible: false, + breadcrumbStatus: 'hidden', + children: [ + { + title: i18n.translate('xpack.serverlessObservability.nav.discover', { + defaultMessage: 'Discover', + }), + link: 'last-used-logs-viewer', + // avoid duplicate "Discover" breadcrumbs + breadcrumbStatus: 'hidden', + renderAs: 'item', + children: [ + { + link: 'discover', + children: [ + { + link: 'observability-logs-explorer', + }, + ], + }, + ], }, - }, - { - link: 'observability-overview:alerts', - }, - { - link: 'observability-overview:cases', - renderAs: 'item', - children: [ - { - link: 'observability-overview:cases_configure', - }, - { - link: 'observability-overview:cases_create', - }, - ], - }, - { - title: i18n.translate('xpack.serverlessObservability.nav.slo', { - defaultMessage: 'SLOs', - }), - link: 'slo', - }, - { - link: 'observabilityAIAssistant', - title: i18n.translate('xpack.serverlessObservability.nav.aiAssistant', { - defaultMessage: 'AI Assistant', - }), - }, - { link: 'inventory', spaceBefore: 'm' }, - { - id: 'apm', - title: i18n.translate('xpack.serverlessObservability.nav.applications', { - defaultMessage: 'Applications', - }), - renderAs: 'panelOpener', - children: [ - { - children: [ - { - link: 'apm:services', - title: i18n.translate('xpack.serverlessObservability.nav.apm.services', { - defaultMessage: 'Service inventory', - }), - }, - { link: 'apm:traces' }, - { link: 'apm:dependencies' }, - { link: 'apm:settings' }, - { - id: 'synthetics', - title: i18n.translate('xpack.serverlessObservability.nav.synthetics', { - defaultMessage: 'Synthetics', - }), - children: [ - { - title: i18n.translate( - 'xpack.serverlessObservability.nav.synthetics.overviewItem', - { - defaultMessage: 'Overview', - } - ), - id: 'synthetics-overview', - link: 'synthetics:overview', - breadcrumbStatus: 'hidden', - }, - { - link: 'synthetics:certificates', - title: i18n.translate( - 'xpack.serverlessObservability.nav.synthetics.certificatesItem', - { - defaultMessage: 'TLS certificates', - } - ), - id: 'synthetics-certificates', - breadcrumbStatus: 'hidden', - }, - ], - }, - ], + { + title: i18n.translate('xpack.serverlessObservability.nav.dashboards', { + defaultMessage: 'Dashboards', + }), + link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); }, - ], - }, - { - id: 'metrics', - title: i18n.translate('xpack.serverlessObservability.nav.infrastructure', { - defaultMessage: 'Infrastructure', - }), - renderAs: 'panelOpener', - children: [ - { - children: [ - { - link: 'metrics:inventory', - title: i18n.translate( - 'xpack.serverlessObservability.nav.infrastructureInventory', - { - defaultMessage: 'Infrastructure inventory', - } - ), - }, - { link: 'metrics:hosts' }, - { link: 'metrics:settings' }, - { link: 'metrics:assetDetails' }, - ], - }, - ], - }, - { - id: 'machine_learning-landing', - renderAs: 'panelOpener', - title: i18n.translate('xpack.serverlessObservability.nav.machineLearning', { - defaultMessage: 'Machine learning', - }), - children: [ - { - children: [ - { - link: 'ml:overview', - }, - { - link: 'ml:notifications', - }, - { - link: 'ml:memoryUsage', - title: i18n.translate( - 'xpack.serverlessObservability.nav.machineLearning.memoryUsage', - { - defaultMessage: 'Memory usage', - } - ), - }, - ], - }, - { - id: 'category-anomaly_detection', - title: i18n.translate('xpack.serverlessObservability.nav.ml.anomaly_detection', { - defaultMessage: 'Anomaly detection', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:anomalyDetection', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.anomaly_detection.jobs', - { - defaultMessage: 'Jobs', - } - ), - }, - { - link: 'ml:anomalyExplorer', - }, - { - link: 'ml:singleMetricViewer', - }, - { - link: 'ml:settings', - }, - { - link: 'ml:suppliedConfigurations', - }, - ], - }, - { - id: 'category-data_frame analytics', - title: i18n.translate('xpack.serverlessObservability.nav.ml.data_frame_analytics', { - defaultMessage: 'Data frame analytics', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:dataFrameAnalytics', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.data_frame_analytics.jobs', - { - defaultMessage: 'Jobs', - } - ), - }, - { - link: 'ml:resultExplorer', - }, - { - link: 'ml:analyticsMap', - }, - ], - }, - { - id: 'category-model_management', - title: i18n.translate('xpack.serverlessObservability.nav.ml.model_management', { - defaultMessage: 'Model management', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:nodesOverview', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.model_management.trainedModels', - { - defaultMessage: 'Trained models', - } - ), - }, - ], - }, - { - id: 'category-data_visualizer', - title: i18n.translate('xpack.serverlessObservability.nav.ml.data_visualizer', { - defaultMessage: 'Data visualizer', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:fileUpload', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.data_visualizer.file_data_visualizer', - { - defaultMessage: 'File data visualizer', - } - ), - }, - { - link: 'ml:indexDataVisualizer', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.data_visualizer.data_view_data_visualizer', - { - defaultMessage: 'Data view data visualizer', - } - ), - }, - { - link: 'ml:dataDrift', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.data_visualizer.data_drift', - { - defaultMessage: 'Data drift', - } - ), - }, - ], - }, - { - id: 'category-aiops_labs', - title: i18n.translate('xpack.serverlessObservability.nav.ml.aiops_labs', { - defaultMessage: 'Aiops labs', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:logRateAnalysis', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.aiops_labs.log_rate_analysis', - { - defaultMessage: 'Log rate analysis', - } - ), - }, - { - link: 'ml:logPatternAnalysis', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.aiops_labs.log_pattern_analysis', - { - defaultMessage: 'Log pattern analysis', - } - ), - }, + }, + { + link: 'observability-overview:alerts', + }, + { + link: 'observability-overview:cases', + renderAs: 'item', + children: [ + { + link: 'observability-overview:cases_configure', + }, + { + link: 'observability-overview:cases_create', + }, + ], + }, + { + title: i18n.translate('xpack.serverlessObservability.nav.slo', { + defaultMessage: 'SLOs', + }), + link: 'slo', + }, + { + link: 'observabilityAIAssistant', + title: i18n.translate('xpack.serverlessObservability.nav.aiAssistant', { + defaultMessage: 'AI Assistant', + }), + }, + { link: 'inventory', spaceBefore: 'm' }, + ...(streamsAvailable + ? [ { - link: 'ml:changePointDetections', - title: i18n.translate( - 'xpack.serverlessObservability.nav.ml.aiops_labs.change_point_detection', - { - defaultMessage: 'Change point detection', - } - ), + link: 'streams' as const, }, - ], - }, - ], - }, - ], - }, - ], - footer: [ - { - type: 'navItem', - title: i18n.translate('xpack.serverlessObservability.nav.getStarted', { - defaultMessage: 'Add data', - }), - link: 'observabilityOnboarding', - icon: 'launch', - }, - { - type: 'navItem', - id: 'devTools', - title: i18n.translate('xpack.serverlessObservability.nav.devTools', { - defaultMessage: 'Developer tools', - }), - link: 'dev_tools', - icon: 'editorCodeBlock', - }, - { - type: 'navGroup', - id: 'project_settings_project_nav', - title: i18n.translate('xpack.serverlessObservability.nav.projectSettings', { - defaultMessage: 'Project settings', - }), - icon: 'gear', - breadcrumbStatus: 'hidden', - children: [ - { - link: 'management', - title: i18n.translate('xpack.serverlessObservability.nav.mngt', { - defaultMessage: 'Management', - }), - }, - { - link: 'integrations', - }, - { - link: 'fleet', - }, - { - id: 'cloudLinkUserAndRoles', - cloudLink: 'userAndRoles', - }, - { - id: 'cloudLinkBilling', - cloudLink: 'billingAndSub', - }, - ], - }, - ], + ] + : []), + { + id: 'apm', + title: i18n.translate('xpack.serverlessObservability.nav.applications', { + defaultMessage: 'Applications', + }), + renderAs: 'panelOpener', + children: [ + { + children: [ + { + link: 'apm:services', + title: i18n.translate('xpack.serverlessObservability.nav.apm.services', { + defaultMessage: 'Service inventory', + }), + }, + { link: 'apm:traces' }, + { link: 'apm:dependencies' }, + { link: 'apm:settings' }, + { + id: 'synthetics', + title: i18n.translate('xpack.serverlessObservability.nav.synthetics', { + defaultMessage: 'Synthetics', + }), + children: [ + { + title: i18n.translate( + 'xpack.serverlessObservability.nav.synthetics.overviewItem', + { + defaultMessage: 'Overview', + } + ), + id: 'synthetics-overview', + link: 'synthetics:overview', + breadcrumbStatus: 'hidden', + }, + { + link: 'synthetics:certificates', + title: i18n.translate( + 'xpack.serverlessObservability.nav.synthetics.certificatesItem', + { + defaultMessage: 'TLS certificates', + } + ), + id: 'synthetics-certificates', + breadcrumbStatus: 'hidden', + }, + ], + }, + ], + }, + ], + }, + { + id: 'metrics', + title: i18n.translate('xpack.serverlessObservability.nav.infrastructure', { + defaultMessage: 'Infrastructure', + }), + renderAs: 'panelOpener', + children: [ + { + children: [ + { + link: 'metrics:inventory', + title: i18n.translate( + 'xpack.serverlessObservability.nav.infrastructureInventory', + { + defaultMessage: 'Infrastructure inventory', + } + ), + }, + { link: 'metrics:hosts' }, + { link: 'metrics:settings' }, + { link: 'metrics:assetDetails' }, + ], + }, + ], + }, + { + id: 'machine_learning-landing', + renderAs: 'panelOpener', + title: i18n.translate('xpack.serverlessObservability.nav.machineLearning', { + defaultMessage: 'Machine learning', + }), + children: [ + { + children: [ + { + link: 'ml:overview', + }, + { + link: 'ml:notifications', + }, + { + link: 'ml:memoryUsage', + title: i18n.translate( + 'xpack.serverlessObservability.nav.machineLearning.memoryUsage', + { + defaultMessage: 'Memory usage', + } + ), + }, + ], + }, + { + id: 'category-anomaly_detection', + title: i18n.translate('xpack.serverlessObservability.nav.ml.anomaly_detection', { + defaultMessage: 'Anomaly detection', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:anomalyDetection', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.anomaly_detection.jobs', + { + defaultMessage: 'Jobs', + } + ), + }, + { + link: 'ml:anomalyExplorer', + }, + { + link: 'ml:singleMetricViewer', + }, + { + link: 'ml:settings', + }, + { + link: 'ml:suppliedConfigurations', + }, + ], + }, + { + id: 'category-data_frame analytics', + title: i18n.translate('xpack.serverlessObservability.nav.ml.data_frame_analytics', { + defaultMessage: 'Data frame analytics', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:dataFrameAnalytics', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.data_frame_analytics.jobs', + { + defaultMessage: 'Jobs', + } + ), + }, + { + link: 'ml:resultExplorer', + }, + { + link: 'ml:analyticsMap', + }, + ], + }, + { + id: 'category-model_management', + title: i18n.translate('xpack.serverlessObservability.nav.ml.model_management', { + defaultMessage: 'Model management', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:nodesOverview', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.model_management.trainedModels', + { + defaultMessage: 'Trained models', + } + ), + }, + ], + }, + { + id: 'category-data_visualizer', + title: i18n.translate('xpack.serverlessObservability.nav.ml.data_visualizer', { + defaultMessage: 'Data visualizer', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:fileUpload', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.data_visualizer.file_data_visualizer', + { + defaultMessage: 'File data visualizer', + } + ), + }, + { + link: 'ml:indexDataVisualizer', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.data_visualizer.data_view_data_visualizer', + { + defaultMessage: 'Data view data visualizer', + } + ), + }, + { + link: 'ml:dataDrift', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.data_visualizer.data_drift', + { + defaultMessage: 'Data drift', + } + ), + }, + ], + }, + { + id: 'category-aiops_labs', + title: i18n.translate('xpack.serverlessObservability.nav.ml.aiops_labs', { + defaultMessage: 'Aiops labs', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:logRateAnalysis', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.aiops_labs.log_rate_analysis', + { + defaultMessage: 'Log rate analysis', + } + ), + }, + { + link: 'ml:logPatternAnalysis', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.aiops_labs.log_pattern_analysis', + { + defaultMessage: 'Log pattern analysis', + } + ), + }, + { + link: 'ml:changePointDetections', + title: i18n.translate( + 'xpack.serverlessObservability.nav.ml.aiops_labs.change_point_detection', + { + defaultMessage: 'Change point detection', + } + ), + }, + ], + }, + ], + }, + ], + }, + ], + footer: [ + { + type: 'navItem', + title: i18n.translate('xpack.serverlessObservability.nav.getStarted', { + defaultMessage: 'Add data', + }), + link: 'observabilityOnboarding', + icon: 'launch', + }, + { + type: 'navItem', + id: 'devTools', + title: i18n.translate('xpack.serverlessObservability.nav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.serverlessObservability.nav.projectSettings', { + defaultMessage: 'Project settings', + }), + icon: 'gear', + breadcrumbStatus: 'hidden', + children: [ + { + link: 'management', + title: i18n.translate('xpack.serverlessObservability.nav.mngt', { + defaultMessage: 'Management', + }), + }, + { + link: 'integrations', + }, + { + link: 'fleet', + }, + { + id: 'cloudLinkUserAndRoles', + cloudLink: 'userAndRoles', + }, + { + id: 'cloudLinkBilling', + cloudLink: 'billingAndSub', + }, + ], + }, + ], + }; }; diff --git a/x-pack/plugins/serverless_observability/public/plugin.ts b/x-pack/plugins/serverless_observability/public/plugin.ts index 05d598b2b3a7e..774a76749f8d6 100644 --- a/x-pack/plugins/serverless_observability/public/plugin.ts +++ b/x-pack/plugins/serverless_observability/public/plugin.ts @@ -8,8 +8,8 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { appCategories, appIds } from '@kbn/management-cards-navigation'; -import { of } from 'rxjs'; -import { navigationTree } from './navigation_tree'; +import { map, of } from 'rxjs'; +import { createNavigationTree } from './navigation_tree'; import { createObservabilityDashboardRegistration } from './logs_signal/overview_registration'; import { ServerlessObservabilityPublicSetup, @@ -50,7 +50,11 @@ export class ServerlessObservabilityPlugin setupDeps: ServerlessObservabilityPublicStartDependencies ): ServerlessObservabilityPublicStart { const { serverless, management, security } = setupDeps; - const navigationTree$ = of(navigationTree); + const navigationTree$ = (setupDeps.streams?.status$ || of({ status: 'disabled' })).pipe( + map(({ status }) => { + return createNavigationTree({ streamsAvailable: status === 'enabled' }); + }) + ); serverless.setProjectHome('/app/observability/landing'); serverless.initNavigation('oblt', navigationTree$, { dataTestSubj: 'svlObservabilitySideNav' }); const aiAssistantIsEnabled = core.application.capabilities.observabilityAIAssistant?.show; diff --git a/x-pack/plugins/serverless_observability/public/types.ts b/x-pack/plugins/serverless_observability/public/types.ts index c93865f0f596e..23da6c12637d3 100644 --- a/x-pack/plugins/serverless_observability/public/types.ts +++ b/x-pack/plugins/serverless_observability/public/types.ts @@ -8,13 +8,14 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DiscoverSetup } from '@kbn/discover-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import { ObservabilityPublicSetup } from '@kbn/observability-plugin/public'; -import { +import type { ObservabilityPublicSetup } from '@kbn/observability-plugin/public'; +import type { ObservabilitySharedPluginSetup, ObservabilitySharedPluginStart, } from '@kbn/observability-shared-plugin/public'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { StreamsPluginStart, StreamsPluginSetup } from '@kbn/streams-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ServerlessObservabilityPublicSetup {} @@ -28,6 +29,7 @@ export interface ServerlessObservabilityPublicSetupDependencies { serverless: ServerlessPluginSetup; management: ManagementSetup; discover: DiscoverSetup; + streams?: StreamsPluginSetup; } export interface ServerlessObservabilityPublicStartDependencies { @@ -36,4 +38,5 @@ export interface ServerlessObservabilityPublicStartDependencies { management: ManagementStart; data: DataPublicPluginStart; security: SecurityPluginStart; + streams?: StreamsPluginStart; } diff --git a/x-pack/plugins/serverless_observability/tsconfig.json b/x-pack/plugins/serverless_observability/tsconfig.json index 3d909bf88b656..5aa97143107ae 100644 --- a/x-pack/plugins/serverless_observability/tsconfig.json +++ b/x-pack/plugins/serverless_observability/tsconfig.json @@ -30,5 +30,6 @@ "@kbn/discover-plugin", "@kbn/security-plugin", "@kbn/search-types", + "@kbn/streams-plugin", ] } diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts index 72bfa570df5c7..dcc9fe1932f5d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useCollapsibleList } from './use_collapsible_list'; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx b/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx index acb65de2a9a16..988afb8d2182b 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -19,6 +20,7 @@ import { EuiPopoverTitle, EuiText, useEuiPaddingCSS, + useIsWithinBreakpoints, } from '@elastic/eui'; import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { @@ -30,6 +32,9 @@ import { DataViewSelector } from '@kbn/unified-search-plugin/public'; import type { DataViewListItemEnhanced } from '@kbn/unified-search-plugin/public/dataview_picker/dataview_list'; import { EsQueryRuleMetaData } from '../es_query/types'; +const DESKTOP_WIDTH = 450; +const MOBILE_WIDTH = 350; + export interface DataViewSelectPopoverProps { dependencies: { dataViews: DataViewsPublicPluginStart; @@ -61,6 +66,8 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove const [dataViewItems, setDataViewsItems] = useState<DataViewListItemEnhanced[]>([]); const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false); + const isMobile = useIsWithinBreakpoints(['xs']); + const closeDataViewEditor = useRef<() => void | undefined>(); const allDataViewItems = useMemo( @@ -179,9 +186,14 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove anchorPosition="downLeft" display="block" > - <div style={{ width: '450px' }} data-test-subj="chooseDataViewPopoverContent"> + <div + css={css` + width: ${isMobile ? `${MOBILE_WIDTH}px` : `${DESKTOP_WIDTH}px`}; + `} + data-test-subj="chooseDataViewPopoverContent" + > <EuiPopoverTitle> - <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> <EuiFlexItem> {i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPopoverTitle', { defaultMessage: 'Data view', diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts index f476d7d896b69..b22c1a81537c1 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/test_query_row/use_test_query.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { act } from 'react-test-renderer'; import { useTestQuery } from './use_test_query'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/use_email_config.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/email/use_email_config.test.ts index ee5ba95bf577b..0830b68fc1d56 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/use_email_config.test.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/use_email_config.test.ts @@ -6,7 +6,7 @@ */ import { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useEmailConfig } from './use_email_config'; const http = httpServiceMock.createStartContract(); @@ -16,6 +16,12 @@ const renderUseEmailConfigHook = (currentService?: string) => renderHook(() => useEmailConfig({ http, toasts })); describe('useEmailConfig', () => { + let res: ReturnType<ReturnType<typeof useEmailConfig>['getEmailServiceConfig']> extends Promise< + infer R + > + ? R + : never; + beforeEach(() => jest.clearAllMocks()); it('should return the correct result when requesting the config of a service', async () => { @@ -26,14 +32,16 @@ describe('useEmailConfig', () => { }); const { result } = renderUseEmailConfigHook(); + await act(async () => { - const res = await result.current.getEmailServiceConfig('gmail'); - expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/gmail'); - expect(res).toEqual({ - host: 'smtp.gmail.com', - port: 465, - secure: true, - }); + res = await result.current.getEmailServiceConfig('gmail'); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/gmail'); + expect(res).toEqual({ + host: 'smtp.gmail.com', + port: 465, + secure: true, }); }); @@ -44,14 +52,16 @@ describe('useEmailConfig', () => { }); const { result } = renderUseEmailConfigHook(); + await act(async () => { - const res = await result.current.getEmailServiceConfig('gmail'); - expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/gmail'); - expect(res).toEqual({ - host: 'smtp.gmail.com', - port: 465, - secure: false, - }); + res = await result.current.getEmailServiceConfig('gmail'); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/gmail'); + expect(res).toEqual({ + host: 'smtp.gmail.com', + port: 465, + secure: false, }); }); @@ -60,13 +70,14 @@ describe('useEmailConfig', () => { const { result } = renderUseEmailConfigHook(); await act(async () => { - const res = await result.current.getEmailServiceConfig('foo'); - expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/foo'); - expect(res).toEqual({ - host: '', - port: 0, - secure: false, - }); + res = await result.current.getEmailServiceConfig('foo'); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/stack_connectors/_email_config/foo'); + expect(res).toEqual({ + host: '', + port: 0, + secure: false, }); }); @@ -75,13 +86,13 @@ describe('useEmailConfig', () => { throw new Error('no!'); }); - const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + const { result } = renderUseEmailConfigHook(); await act(async () => { result.current.getEmailServiceConfig('foo'); - await waitForNextUpdate(); - expect(toasts.addDanger).toHaveBeenCalled(); }); + + await waitFor(() => expect(toasts.addDanger).toHaveBeenCalled()); }); it('should not make an API call if the service is empty', async () => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts index 18bcdc6232792..39706e6d22c05 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useGetDashboard } from './use_get_dashboard'; import { getDashboard } from './api'; import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; @@ -58,44 +58,44 @@ describe('useGetDashboard', () => { ])( 'fetches the %p dashboard and sets the dashboard URL with %p', async (selectedProvider, urlKey) => { - const { result, waitForNextUpdate } = renderHook(() => - useGetDashboard({ ...defaultArgs, selectedProvider }) - ); - await waitForNextUpdate(); - expect(mockDashboard).toHaveBeenCalledWith( - expect.objectContaining({ - connectorId, + const { result } = renderHook(() => useGetDashboard({ ...defaultArgs, selectedProvider })); + await waitFor(() => { + expect(mockDashboard).toHaveBeenCalledWith( + expect.objectContaining({ + connectorId, + dashboardId: `generative-ai-token-usage-${urlKey}-space`, + }) + ); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ + query: { + language: 'kuery', + query: `kibana.saved_objects: { id : ${connectorId} }`, + }, dashboardId: `generative-ai-token-usage-${urlKey}-space`, - }) - ); - expect(mockGetRedirectUrl).toHaveBeenCalledWith({ - query: { - language: 'kuery', - query: `kibana.saved_objects: { id : ${connectorId} }`, - }, - dashboardId: `generative-ai-token-usage-${urlKey}-space`, + }); + expect(result.current.isLoading).toBe(false); + expect(result.current.dashboardUrl).toBe( + `http://localhost:5601/app/dashboards#/view/generative-ai-token-usage-${urlKey}-space` + ); }); - expect(result.current.isLoading).toBe(false); - expect(result.current.dashboardUrl).toBe( - `http://localhost:5601/app/dashboards#/view/generative-ai-token-usage-${urlKey}-space` - ); } ); it('handles the case where the dashboard is not available.', async () => { mockDashboard.mockResolvedValue({ data: { available: false } }); - const { result, waitForNextUpdate } = renderHook(() => useGetDashboard(defaultArgs)); - await waitForNextUpdate(); - expect(mockDashboard).toHaveBeenCalledWith( - expect.objectContaining({ - connectorId, - dashboardId: 'generative-ai-token-usage-openai-space', - }) - ); - expect(mockGetRedirectUrl).not.toHaveBeenCalled(); + const { result } = renderHook(() => useGetDashboard(defaultArgs)); + await waitFor(() => { + expect(mockDashboard).toHaveBeenCalledWith( + expect.objectContaining({ + connectorId, + dashboardId: 'generative-ai-token-usage-openai-space', + }) + ); + expect(mockGetRedirectUrl).not.toHaveBeenCalled(); - expect(result.current.isLoading).toBe(false); - expect(result.current.dashboardUrl).toBe(null); + expect(result.current.isLoading).toBe(false); + expect(result.current.dashboardUrl).toBe(null); + }); }); it('handles the case where the spaces API is not available.', async () => { @@ -111,34 +111,35 @@ describe('useGetDashboard', () => { }); it('handles the case where connectorId is empty string', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetDashboard({ ...defaultArgs, connectorId: '' }) - ); - await waitForNextUpdate(); - expect(mockDashboard).not.toHaveBeenCalled(); - expect(mockGetRedirectUrl).not.toHaveBeenCalled(); - expect(result.current.isLoading).toBe(false); - expect(result.current.dashboardUrl).toBe(null); + const { result } = renderHook(() => useGetDashboard({ ...defaultArgs, connectorId: '' })); + await waitFor(() => { + expect(mockDashboard).not.toHaveBeenCalled(); + expect(mockGetRedirectUrl).not.toHaveBeenCalled(); + expect(result.current.isLoading).toBe(false); + expect(result.current.dashboardUrl).toBe(null); + }); }); it('handles the case where the dashboard locator is not available.', async () => { mockKibana.mockReturnValue({ services: { ...mockServices, dashboard: {} }, }); - const { result, waitForNextUpdate } = renderHook(() => useGetDashboard(defaultArgs)); - await waitForNextUpdate(); - expect(result.current.isLoading).toBe(false); - expect(result.current.dashboardUrl).toBe(null); + const { result } = renderHook(() => useGetDashboard(defaultArgs)); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.dashboardUrl).toBe(null); + }); }); it('correctly handles errors and displays the appropriate toast messages.', async () => { mockDashboard.mockRejectedValue(new Error('Error fetching dashboard')); - const { result, waitForNextUpdate } = renderHook(() => useGetDashboard(defaultArgs)); - await waitForNextUpdate(); - expect(result.current.isLoading).toBe(false); - expect(mockToasts.addDanger).toHaveBeenCalledWith({ - title: 'Error finding OpenAI Token Usage Dashboard.', - text: 'Error fetching dashboard', + const { result } = renderHook(() => useGetDashboard(defaultArgs)); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(mockToasts.addDanger).toHaveBeenCalledWith({ + title: 'Error finding OpenAI Token Usage Dashboard.', + text: 'Error fetching dashboard', + }); }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx index 721897ece7266..3033cebd3ccf6 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { useChoices, UseChoices, UseChoicesProps } from './use_choices'; +import { useChoices } from './use_choices'; import { getChoices } from './api'; jest.mock('./api'); @@ -73,7 +73,7 @@ describe('UseChoices', () => { const fields = ['category']; it('init', async () => { - const { result, waitForNextUpdate } = renderHook<UseChoicesProps, UseChoices>(() => + const { result } = renderHook(() => useChoices({ http: services.http, actionConnector, @@ -82,13 +82,11 @@ describe('UseChoices', () => { }) ); - await waitForNextUpdate(); - - expect(result.current).toEqual(useChoicesResponse); + await waitFor(() => expect(result.current).toEqual(useChoicesResponse)); }); it('returns an empty array if the field is not in response', async () => { - const { result, waitForNextUpdate } = renderHook<UseChoicesProps, UseChoices>(() => + const { result } = renderHook(() => useChoices({ http: services.http, actionConnector, @@ -97,16 +95,16 @@ describe('UseChoices', () => { }) ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - isLoading: false, - choices: { priority: [], category: getChoicesResponse }, - }); + await waitFor(() => + expect(result.current).toEqual({ + isLoading: false, + choices: { priority: [], category: getChoicesResponse }, + }) + ); }); it('returns an empty array when connector is not presented', async () => { - const { result } = renderHook<UseChoicesProps, UseChoices>(() => + const { result } = renderHook(() => useChoices({ http: services.http, actionConnector: undefined, @@ -127,7 +125,7 @@ describe('UseChoices', () => { serviceMessage: 'An error occurred', }); - const { waitForNextUpdate } = renderHook<UseChoicesProps, UseChoices>(() => + renderHook(() => useChoices({ http: services.http, actionConnector, @@ -136,12 +134,12 @@ describe('UseChoices', () => { }) ); - await waitForNextUpdate(); - - expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'An error occurred', - title: 'Unable to get choices', - }); + await waitFor(() => + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }) + ); }); it('it displays an error when http throws an error', async () => { @@ -149,7 +147,7 @@ describe('UseChoices', () => { throw new Error('An error occurred'); }); - renderHook<UseChoicesProps, UseChoices>(() => + renderHook(() => useChoices({ http: services.http, actionConnector, @@ -158,9 +156,11 @@ describe('UseChoices', () => { }) ); - expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'An error occurred', - title: 'Unable to get choices', - }); + await waitFor(() => + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }) + ); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx index c8c061c9d07f1..c4cf65a591338 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; -import { useGetAppInfo, UseGetAppInfo, UseGetAppInfoProps } from './use_get_app_info'; +import { useGetAppInfo } from './use_get_app_info'; import { getAppInfo } from './api'; import { ServiceNowActionConnector } from './types'; import { httpServiceMock } from '@kbn/core/public/mocks'; @@ -50,7 +50,7 @@ describe('useGetAppInfo', () => { }); it('init', async () => { - const { result } = renderHook<UseGetAppInfoProps, UseGetAppInfo>(() => + const { result } = renderHook(() => useGetAppInfo({ actionTypeId, http, @@ -64,7 +64,7 @@ describe('useGetAppInfo', () => { }); it('returns the application information', async () => { - const { result } = renderHook<UseGetAppInfoProps, UseGetAppInfo>(() => + const { result } = renderHook(() => useGetAppInfo({ actionTypeId, http, @@ -86,7 +86,7 @@ describe('useGetAppInfo', () => { throw new Error('An error occurred'); }); - const { result } = renderHook<UseGetAppInfoProps, UseGetAppInfo>(() => + const { result } = renderHook(() => useGetAppInfo({ actionTypeId, http, @@ -108,7 +108,7 @@ describe('useGetAppInfo', () => { throw error; }); - const { result } = renderHook<UseGetAppInfoProps, UseGetAppInfo>(() => + const { result } = renderHook(() => useGetAppInfo({ actionTypeId, http, diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx index 38ea6d55b4e17..fd9808139b8ba 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices'; +import { useGetChoices } from './use_get_choices'; import { getChoices } from './api'; jest.mock('./api'); @@ -69,7 +69,7 @@ describe('useGetChoices', () => { const fields = ['priority']; it('init', async () => { - const { result, waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() => + const { result } = renderHook(() => useGetChoices({ http: services.http, actionConnector, @@ -79,16 +79,16 @@ describe('useGetChoices', () => { }) ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - isLoading: false, - choices: getChoicesResponse, - }); + await waitFor(() => + expect(result.current).toEqual({ + isLoading: false, + choices: getChoicesResponse, + }) + ); }); it('returns an empty array when connector is not presented', async () => { - const { result } = renderHook<UseGetChoicesProps, UseGetChoices>(() => + const { result } = renderHook(() => useGetChoices({ http: services.http, actionConnector: undefined, @@ -105,7 +105,7 @@ describe('useGetChoices', () => { }); it('it calls onSuccess', async () => { - const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() => + renderHook(() => useGetChoices({ http: services.http, actionConnector, @@ -115,9 +115,7 @@ describe('useGetChoices', () => { }) ); - await waitForNextUpdate(); - - expect(onSuccess).toHaveBeenCalledWith(getChoicesResponse); + await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(getChoicesResponse)); }); it('it displays an error when service fails', async () => { @@ -126,7 +124,7 @@ describe('useGetChoices', () => { serviceMessage: 'An error occurred', }); - const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() => + renderHook(() => useGetChoices({ http: services.http, actionConnector, @@ -136,12 +134,12 @@ describe('useGetChoices', () => { }) ); - await waitForNextUpdate(); - - expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'An error occurred', - title: 'Unable to get choices', - }); + await waitFor(() => + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }) + ); }); it('it displays an error when http throws an error', async () => { @@ -149,7 +147,7 @@ describe('useGetChoices', () => { throw new Error('An error occurred'); }); - renderHook<UseGetChoicesProps, UseGetChoices>(() => + renderHook(() => useGetChoices({ http: services.http, actionConnector, @@ -159,10 +157,12 @@ describe('useGetChoices', () => { }) ); - expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'An error occurred', - title: 'Unable to get choices', - }); + await waitFor(() => + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }) + ); }); it('returns an empty array if the response is not an array', async () => { @@ -171,7 +171,7 @@ describe('useGetChoices', () => { data: {}, }); - const { result } = renderHook<UseGetChoicesProps, UseGetChoices>(() => + const { result } = renderHook(() => useGetChoices({ http: services.http, actionConnector: undefined, @@ -181,9 +181,11 @@ describe('useGetChoices', () => { }) ); - expect(result.current).toEqual({ - isLoading: false, - choices: [], + await waitFor(() => { + expect(result.current).toEqual({ + isLoading: false, + choices: [], + }); }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/swimlane/use_get_application.test.tsx index 82e514ec51fd9..db33c7aa68014 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/swimlane/use_get_application.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/swimlane/use_get_application.test.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana'; import { getApplication } from './api'; -import { useGetApplication, UseGetApplication } from './use_get_application'; +import { useGetApplication } from './use_get_application'; jest.mock('./api'); jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana'); @@ -43,87 +43,86 @@ describe('useGetApplication', () => { }); it('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); - - await waitForNextUpdate(); - expect(result.current).toEqual({ - isLoading: false, - getApplication: result.current.getApplication, - }); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, }); }); it('calls getApplication with correct arguments', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); + act(() => { result.current.getApplication({ appId: action.config.appId, apiToken: action.secrets.apiToken, apiUrl: action.config.apiUrl, }); + }); - await waitForNextUpdate(); + await waitFor(() => expect(getApplicationMock).toBeCalledWith({ signal: abortCtrl.signal, appId: action.config.appId, apiToken: action.secrets.apiToken, url: action.config.apiUrl, - }); - }); + }) + ); }); it('get application', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); - - await waitForNextUpdate(); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); + + await waitFor(() => new Promise((resolve) => resolve(null))); + act(() => { result.current.getApplication({ appId: action.config.appId, apiToken: action.secrets.apiToken, apiUrl: action.config.apiUrl, }); - await waitForNextUpdate(); - + }); + await waitFor(() => expect(result.current).toEqual({ isLoading: false, getApplication: result.current.getApplication, - }); - }); + }) + ); }); it('set isLoading to true when getting the application', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); - - await waitForNextUpdate(); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); + + await waitFor(() => new Promise((resolve) => resolve(null))); + + act(() => { result.current.getApplication({ appId: action.config.appId, apiToken: action.secrets.apiToken, apiUrl: action.config.apiUrl, }); - - expect(result.current.isLoading).toBe(true); }); + + expect(result.current.isLoading).toBe(true); }); it('it displays an error when http throws an error', async () => { @@ -131,52 +130,52 @@ describe('useGetApplication', () => { throw new Error('Something went wrong'); }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); + await waitFor(() => new Promise((resolve) => resolve(null))); + act(() => { result.current.getApplication({ appId: action.config.appId, apiToken: action.secrets.apiToken, apiUrl: action.config.apiUrl, }); + }); - expect(result.current).toEqual({ - isLoading: false, - getApplication: result.current.getApplication, - }); + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); - expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - title: 'Unable to get application with id bcq16kdTbz5jlwM6h', - text: 'Something went wrong', - }); + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Something went wrong', }); }); it('it displays an error when the response does not contain the correct fields', async () => { getApplicationMock.mockResolvedValue({}); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => - useGetApplication({ - toastNotifications: services.notifications.toasts, - }) - ); - await waitForNextUpdate(); + const { result } = renderHook(() => + useGetApplication({ + toastNotifications: services.notifications.toasts, + }) + ); + await waitFor(() => new Promise((resolve) => resolve(null))); + act(() => { result.current.getApplication({ appId: action.config.appId, apiToken: action.secrets.apiToken, apiUrl: action.config.apiUrl, }); - await waitForNextUpdate(); - + }); + await waitFor(() => expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ title: 'Unable to get application with id bcq16kdTbz5jlwM6h', text: 'Unable to get application fields', - }); - }); + }) + ); }); }); diff --git a/x-pack/plugins/streams/common/index.ts b/x-pack/plugins/streams/common/index.ts new file mode 100644 index 0000000000000..3a7306e46cae2 --- /dev/null +++ b/x-pack/plugins/streams/common/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { StreamDefinition } from './types'; diff --git a/x-pack/plugins/streams/common/types.ts b/x-pack/plugins/streams/common/types.ts index 6cdb2f923f6f4..d3aa43911ec2c 100644 --- a/x-pack/plugins/streams/common/types.ts +++ b/x-pack/plugins/streams/common/types.ts @@ -72,7 +72,7 @@ export const streamWithoutIdDefinitonSchema = z.object({ .array( z.object({ id: z.string(), - condition: conditionSchema, + condition: z.optional(conditionSchema), }) ) .default([]), diff --git a/x-pack/plugins/streams/public/api/index.ts b/x-pack/plugins/streams/public/api/index.ts new file mode 100644 index 0000000000000..f64fc7f2fdce8 --- /dev/null +++ b/x-pack/plugins/streams/public/api/index.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, CoreStart, HttpFetchOptions } from '@kbn/core/public'; +import type { + ClientRequestParamsOf, + ReturnOf, + RouteRepositoryClient, +} from '@kbn/server-route-repository'; +import { createRepositoryClient } from '@kbn/server-route-repository-client'; +import type { StreamsRouteRepository } from '../../server'; + +type FetchOptions = Omit<HttpFetchOptions, 'body'> & { + body?: any; +}; + +export type StreamsRepositoryClientOptions = Omit< + FetchOptions, + 'query' | 'body' | 'pathname' | 'signal' +> & { + signal: AbortSignal | null; +}; + +export type StreamsRepositoryClient = RouteRepositoryClient< + StreamsRouteRepository, + StreamsRepositoryClientOptions +>; + +export type AutoAbortedStreamsRepositoryClient = RouteRepositoryClient< + StreamsRouteRepository, + Omit<StreamsRepositoryClientOptions, 'signal'> +>; + +export type StreamsRepositoryEndpoint = keyof StreamsRouteRepository; + +export type APIReturnType<TEndpoint extends StreamsRepositoryEndpoint> = ReturnOf< + StreamsRouteRepository, + TEndpoint +>; + +export type StreamsAPIClientRequestParamsOf<TEndpoint extends StreamsRepositoryEndpoint> = + ClientRequestParamsOf<StreamsRouteRepository, TEndpoint>; + +export function createStreamsRepositoryClient( + core: CoreStart | CoreSetup +): StreamsRepositoryClient { + return createRepositoryClient(core); +} diff --git a/x-pack/plugins/streams/public/index.ts b/x-pack/plugins/streams/public/index.ts index 5b83ea1d297d3..bc90fb0f40066 100644 --- a/x-pack/plugins/streams/public/index.ts +++ b/x-pack/plugins/streams/public/index.ts @@ -7,7 +7,12 @@ import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; import { Plugin } from './plugin'; +import { StreamsPluginSetup, StreamsPluginStart } from './types'; -export const plugin: PluginInitializer<{}, {}> = (context: PluginInitializerContext) => { +export type { StreamsPluginSetup, StreamsPluginStart }; + +export const plugin: PluginInitializer<StreamsPluginSetup, StreamsPluginStart> = ( + context: PluginInitializerContext +) => { return new Plugin(context); }; diff --git a/x-pack/plugins/streams/public/plugin.ts b/x-pack/plugins/streams/public/plugin.ts index f35d18e06ff70..5a2ae3e066845 100644 --- a/x-pack/plugins/streams/public/plugin.ts +++ b/x-pack/plugins/streams/public/plugin.ts @@ -8,25 +8,55 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/public'; import { Logger } from '@kbn/logging'; +import { createRepositoryClient } from '@kbn/server-route-repository-client'; +import { from, shareReplay, startWith } from 'rxjs'; +import { once } from 'lodash'; import type { StreamsPublicConfig } from '../common/config'; import { StreamsPluginClass, StreamsPluginSetup, StreamsPluginStart } from './types'; +import { StreamsRepositoryClient } from './api'; export class Plugin implements StreamsPluginClass { public config: StreamsPublicConfig; public logger: Logger; + private repositoryClient!: StreamsRepositoryClient; + constructor(context: PluginInitializerContext<{}>) { this.config = context.config.get(); this.logger = context.logger.get(); } - setup(core: CoreSetup<StreamsPluginStart>, pluginSetup: StreamsPluginSetup) { - return {}; + setup(core: CoreSetup<{}>, pluginSetup: {}): StreamsPluginSetup { + this.repositoryClient = createRepositoryClient(core); + return { + status$: createStatusObservable(this.logger, this.repositoryClient), + }; } - start(core: CoreStart) { - return {}; + start(core: CoreStart, pluginsStart: {}): StreamsPluginStart { + return { + streamsRepositoryClient: this.repositoryClient, + status$: createStatusObservable(this.logger, this.repositoryClient), + }; } stop() {} } + +const createStatusObservable = once((logger: Logger, repositoryClient: StreamsRepositoryClient) => { + return from( + repositoryClient + .fetch('GET /api/streams/_status', { + signal: new AbortController().signal, + }) + .then( + (response) => ({ + status: response.enabled ? ('enabled' as const) : ('disabled' as const), + }), + (error) => { + logger.error(error); + return { status: 'unknown' as const }; + } + ) + ).pipe(startWith({ status: 'unknown' as const }), shareReplay(1)); +}); diff --git a/x-pack/plugins/streams/public/types.ts b/x-pack/plugins/streams/public/types.ts index 61e5fa94098f0..fc88f2a6c20fe 100644 --- a/x-pack/plugins/streams/public/types.ts +++ b/x-pack/plugins/streams/public/types.ts @@ -6,11 +6,16 @@ */ import type { Plugin as PluginClass } from '@kbn/core/public'; +import { Observable } from 'rxjs'; +import type { StreamsRepositoryClient } from './api'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StreamsPluginSetup {} +export interface StreamsPluginSetup { + status$: Observable<{ status: 'unknown' | 'enabled' | 'disabled' }>; +} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StreamsPluginStart {} +export interface StreamsPluginStart { + streamsRepositoryClient: StreamsRepositoryClient; + status$: Observable<{ status: 'unknown' | 'enabled' | 'disabled' }>; +} -export type StreamsPluginClass = PluginClass<{}, {}, StreamsPluginSetup, StreamsPluginStart>; +export type StreamsPluginClass = PluginClass<StreamsPluginSetup, StreamsPluginStart, {}, {}>; diff --git a/x-pack/plugins/streams/server/index.ts b/x-pack/plugins/streams/server/index.ts index bd8aee304ad15..9ef13c62d6b7b 100644 --- a/x-pack/plugins/streams/server/index.ts +++ b/x-pack/plugins/streams/server/index.ts @@ -17,3 +17,5 @@ export const plugin = async (context: PluginInitializerContext<StreamsConfig>) = const { StreamsPlugin } = await import('./plugin'); return new StreamsPlugin(context); }; + +export type { ListStreamResponse } from './lib/streams/stream_crud'; diff --git a/x-pack/plugins/streams/server/lib/streams/component_templates/generate_layer.ts b/x-pack/plugins/streams/server/lib/streams/component_templates/generate_layer.ts index 82c89c9ab9171..4763aacb44478 100644 --- a/x-pack/plugins/streams/server/lib/streams/component_templates/generate_layer.ts +++ b/x-pack/plugins/streams/server/lib/streams/component_templates/generate_layer.ts @@ -30,7 +30,8 @@ export function generateLayer( template: { settings: isRoot(definition.id) ? logsSettings : {}, mappings: { - subobjects: false, + subobjects: true, // TODO set to false once this works on Elasticsearch side - right now fields are not properly indexed. + dynamic: false, properties, }, }, diff --git a/x-pack/plugins/streams/server/lib/streams/data_streams/manage_data_streams.ts b/x-pack/plugins/streams/server/lib/streams/data_streams/manage_data_streams.ts index 812739db56c73..a9b667906fdf3 100644 --- a/x-pack/plugins/streams/server/lib/streams/data_streams/manage_data_streams.ts +++ b/x-pack/plugins/streams/server/lib/streams/data_streams/manage_data_streams.ts @@ -6,6 +6,7 @@ */ import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; import { retryTransientEsErrors } from '../helpers/retry'; interface DataStreamManagementOptions { @@ -23,6 +24,7 @@ interface DeleteDataStreamOptions { interface RolloverDataStreamOptions { esClient: ElasticsearchClient; name: string; + mappings: MappingTypeMapping['properties'] | undefined; logger: Logger; } @@ -56,38 +58,37 @@ export async function rolloverDataStreamIfNecessary({ esClient, name, logger, + mappings, }: RolloverDataStreamOptions) { const dataStreams = await esClient.indices.getDataStream({ name: `${name},${name}.*` }); for (const dataStream of dataStreams.data_streams) { - const currentMappings = - Object.values( - await esClient.indices.getMapping({ - index: dataStream.indices.at(-1)?.index_name, - }) - )[0].mappings.properties || {}; - const simulatedIndex = await esClient.indices.simulateIndexTemplate({ name: dataStream.name }); - const simulatedMappings = simulatedIndex.template.mappings.properties || {}; - - // check whether the same fields and same types are listed (don't check for other mapping attributes) - const isDifferent = - Object.values(simulatedMappings).length !== Object.values(currentMappings).length || - Object.entries(simulatedMappings || {}).some(([fieldName, { type }]) => { - const currentType = currentMappings[fieldName]?.type; - return currentType !== type; - }); - - if (!isDifferent) { + const writeIndex = dataStream.indices.at(-1); + if (!writeIndex) { continue; } - try { - await retryTransientEsErrors(() => esClient.indices.rollover({ alias: dataStream.name }), { - logger, - }); - logger.debug(() => `Rolled over data stream: ${dataStream.name}`); + await retryTransientEsErrors( + () => esClient.indices.putMapping({ index: writeIndex.index_name, properties: mappings }), + { + logger, + } + ); } catch (error: any) { - logger.error(`Error rolling over data stream: ${error.message}`); - throw error; + if ( + typeof error.message !== 'string' || + !error.message.includes('illegal_argument_exception') + ) { + throw error; + } + try { + await retryTransientEsErrors(() => esClient.indices.rollover({ alias: dataStream.name }), { + logger, + }); + logger.debug(() => `Rolled over data stream: ${dataStream.name}`); + } catch (rolloverError: any) { + logger.error(`Error rolling over data stream: ${error.message}`); + throw error; + } } } } diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.test.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.test.ts index aab7f27f12d14..8c63d7caa8811 100644 --- a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.test.ts +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.test.ts @@ -7,48 +7,48 @@ import { conditionToPainless } from './condition_to_painless'; -const operatorConditionAndResutls = [ +const operatorConditionAndResults = [ { condition: { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' }, - result: 'ctx.log?.logger == "nginx_proxy"', + result: '(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy")', }, { condition: { field: 'log.logger', operator: 'neq' as const, value: 'nginx_proxy' }, - result: 'ctx.log?.logger != "nginx_proxy"', + result: '(ctx.log?.logger !== null && ctx.log?.logger != "nginx_proxy")', }, { condition: { field: 'http.response.status_code', operator: 'lt' as const, value: 500 }, - result: 'ctx.http?.response?.status_code < 500', + result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code < 500)', }, { condition: { field: 'http.response.status_code', operator: 'lte' as const, value: 500 }, - result: 'ctx.http?.response?.status_code <= 500', + result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code <= 500)', }, { condition: { field: 'http.response.status_code', operator: 'gt' as const, value: 500 }, - result: 'ctx.http?.response?.status_code > 500', + result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code > 500)', }, { condition: { field: 'http.response.status_code', operator: 'gte' as const, value: 500 }, - result: 'ctx.http?.response?.status_code >= 500', + result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code >= 500)', }, { condition: { field: 'log.logger', operator: 'startsWith' as const, value: 'nginx' }, - result: 'ctx.log?.logger.startsWith("nginx")', + result: '(ctx.log?.logger !== null && ctx.log?.logger.startsWith("nginx"))', }, { condition: { field: 'log.logger', operator: 'endsWith' as const, value: 'proxy' }, - result: 'ctx.log?.logger.endsWith("proxy")', + result: '(ctx.log?.logger !== null && ctx.log?.logger.endsWith("proxy"))', }, { condition: { field: 'log.logger', operator: 'contains' as const, value: 'proxy' }, - result: 'ctx.log?.logger.contains("proxy")', + result: '(ctx.log?.logger !== null && ctx.log?.logger.contains("proxy"))', }, ]; describe('conditionToPainless', () => { describe('operators', () => { - operatorConditionAndResutls.forEach((setup) => { + operatorConditionAndResults.forEach((setup) => { test(`${setup.condition.operator}`, () => { expect(conditionToPainless(setup.condition)).toEqual(setup.result); }); @@ -65,7 +65,7 @@ describe('conditionToPainless', () => { }; expect( expect(conditionToPainless(condition)).toEqual( - 'ctx.log?.logger == "nginx_proxy" && ctx.log?.level == "error"' + '(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && (ctx.log?.level !== null && ctx.log?.level == "error")' ) ); }); @@ -81,7 +81,7 @@ describe('conditionToPainless', () => { }; expect( expect(conditionToPainless(condition)).toEqual( - 'ctx.log?.logger == "nginx_proxy" || ctx.log?.level == "error"' + '(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") || (ctx.log?.level !== null && ctx.log?.level == "error")' ) ); }); @@ -102,7 +102,7 @@ describe('conditionToPainless', () => { }; expect( expect(conditionToPainless(condition)).toEqual( - 'ctx.log?.logger == "nginx_proxy" && (ctx.log?.level == "error" || ctx.log?.level == "ERROR")' + '(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))' ) ); }); @@ -125,7 +125,7 @@ describe('conditionToPainless', () => { }; expect( expect(conditionToPainless(condition)).toEqual( - '(ctx.log?.logger == "nginx_proxy" || ctx.service?.name == "nginx") && (ctx.log?.level == "error" || ctx.log?.level == "ERROR")' + '((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") || (ctx.service?.name !== null && ctx.service?.name == "nginx")) && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))' ) ); }); diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts index 539ad3603535b..2cccef260d7e1 100644 --- a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts @@ -69,7 +69,7 @@ function toPainless(condition: FilterCondition) { export function conditionToPainless(condition: Condition, nested = false): string { if (isFilterCondition(condition)) { - return toPainless(condition); + return `(${safePainlessField(condition)} !== null && ${toPainless(condition)})`; } if (isAndCondition(condition)) { const and = condition.and.map((filter) => conditionToPainless(filter, true)).join(' && '); diff --git a/x-pack/plugins/streams/server/lib/streams/stream_crud.ts b/x-pack/plugins/streams/server/lib/streams/stream_crud.ts index 78a126905d9a4..da5f74d3e69ed 100644 --- a/x-pack/plugins/streams/server/lib/streams/stream_crud.ts +++ b/x-pack/plugins/streams/server/lib/streams/stream_crud.ts @@ -7,29 +7,29 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; -import { FieldDefinition, StreamDefinition } from '../../../common/types'; import { STREAMS_INDEX } from '../../../common/constants'; -import { DefinitionNotFound } from './errors'; -import { deleteTemplate, upsertTemplate } from './index_templates/manage_index_templates'; +import { FieldDefinition, StreamDefinition } from '../../../common/types'; import { generateLayer } from './component_templates/generate_layer'; -import { generateIngestPipeline } from './ingest_pipelines/generate_ingest_pipeline'; -import { generateReroutePipeline } from './ingest_pipelines/generate_reroute_pipeline'; -import { generateIndexTemplate } from './index_templates/generate_index_template'; import { deleteComponent, upsertComponent } from './component_templates/manage_component_templates'; -import { getIndexTemplateName } from './index_templates/name'; import { getComponentTemplateName } from './component_templates/name'; -import { getProcessingPipelineName, getReroutePipelineName } from './ingest_pipelines/name'; -import { - deleteIngestPipeline, - upsertIngestPipeline, -} from './ingest_pipelines/manage_ingest_pipelines'; -import { getAncestors } from './helpers/hierarchy'; -import { MalformedFields } from './errors/malformed_fields'; import { deleteDataStream, rolloverDataStreamIfNecessary, upsertDataStream, } from './data_streams/manage_data_streams'; +import { DefinitionNotFound } from './errors'; +import { MalformedFields } from './errors/malformed_fields'; +import { getAncestors } from './helpers/hierarchy'; +import { generateIndexTemplate } from './index_templates/generate_index_template'; +import { deleteTemplate, upsertTemplate } from './index_templates/manage_index_templates'; +import { getIndexTemplateName } from './index_templates/name'; +import { generateIngestPipeline } from './ingest_pipelines/generate_ingest_pipeline'; +import { generateReroutePipeline } from './ingest_pipelines/generate_reroute_pipeline'; +import { + deleteIngestPipeline, + upsertIngestPipeline, +} from './ingest_pipelines/manage_ingest_pipelines'; +import { getProcessingPipelineName, getReroutePipelineName } from './ingest_pipelines/name'; interface BaseParams { scopedClusterClient: IScopedClusterClient; @@ -88,29 +88,54 @@ async function upsertInternalStream({ definition, scopedClusterClient }: BasePar type ListStreamsParams = BaseParams; -export async function listStreams({ scopedClusterClient }: ListStreamsParams) { +export interface ListStreamResponse { + total: number; + definitions: StreamDefinition[]; +} + +export async function listStreams({ + scopedClusterClient, +}: ListStreamsParams): Promise<ListStreamResponse> { const response = await scopedClusterClient.asInternalUser.search<StreamDefinition>({ index: STREAMS_INDEX, size: 10000, - fields: ['id'], - _source: false, sort: [{ id: 'asc' }], }); - const definitions = response.hits.hits.map((hit) => hit.fields as { id: string[] }); - return definitions; + const definitions = response.hits.hits.map((hit) => hit._source!); + const total = response.hits.total!; + + return { + definitions, + total: typeof total === 'number' ? total : total.value, + }; } interface ReadStreamParams extends BaseParams { id: string; + skipAccessCheck?: boolean; } -export async function readStream({ id, scopedClusterClient }: ReadStreamParams) { +export interface ReadStreamResponse { + definition: StreamDefinition; +} + +export async function readStream({ + id, + scopedClusterClient, + skipAccessCheck, +}: ReadStreamParams): Promise<ReadStreamResponse> { try { const response = await scopedClusterClient.asInternalUser.get<StreamDefinition>({ id, index: STREAMS_INDEX, }); const definition = response._source as StreamDefinition; + if (!skipAccessCheck) { + const hasAccess = await checkReadAccess({ id, scopedClusterClient }); + if (!hasAccess) { + throw new DefinitionNotFound(`Stream definition for ${id} not found.`); + } + } return { definition, }; @@ -126,12 +151,21 @@ interface ReadAncestorsParams extends BaseParams { id: string; } -export async function readAncestors({ id, scopedClusterClient }: ReadAncestorsParams) { +export interface ReadAncestorsResponse { + ancestors: Array<{ definition: StreamDefinition }>; +} + +export async function readAncestors({ + id, + scopedClusterClient, +}: ReadAncestorsParams): Promise<ReadAncestorsResponse> { const ancestorIds = getAncestors(id); - return await Promise.all( - ancestorIds.map((ancestorId) => readStream({ scopedClusterClient, id: ancestorId })) - ); + return { + ancestors: await Promise.all( + ancestorIds.map((ancestorId) => readStream({ scopedClusterClient, id: ancestorId })) + ), + }; } interface ReadDescendantsParams extends BaseParams { @@ -167,7 +201,7 @@ export async function validateAncestorFields( id: string, fields: FieldDefinition[] ) { - const ancestors = await readAncestors({ + const { ancestors } = await readAncestors({ id, scopedClusterClient, }); @@ -223,6 +257,21 @@ export async function checkStreamExists({ id, scopedClusterClient }: ReadStreamP } } +interface CheckReadAccessParams extends BaseParams { + id: string; +} + +export async function checkReadAccess({ + id, + scopedClusterClient, +}: CheckReadAccessParams): Promise<boolean> { + try { + return await scopedClusterClient.asCurrentUser.indices.exists({ index: id }); + } catch (e) { + return false; + } +} + interface SyncStreamParams { scopedClusterClient: IScopedClusterClient; definition: StreamDefinition; @@ -236,10 +285,11 @@ export async function syncStream({ rootDefinition, logger, }: SyncStreamParams) { + const componentTemplate = generateLayer(definition.id, definition); await upsertComponent({ esClient: scopedClusterClient.asCurrentUser, logger, - component: generateLayer(definition.id, definition), + component: componentTemplate, }); await upsertIngestPipeline({ esClient: scopedClusterClient.asCurrentUser, @@ -282,5 +332,6 @@ export async function syncStream({ esClient: scopedClusterClient.asCurrentUser, name: definition.id, logger, + mappings: componentTemplate.template.mappings?.properties, }); } diff --git a/x-pack/plugins/streams/server/plugin.ts b/x-pack/plugins/streams/server/plugin.ts index ef070984803d5..937f8c22b5be0 100644 --- a/x-pack/plugins/streams/server/plugin.ts +++ b/x-pack/plugins/streams/server/plugin.ts @@ -16,8 +16,7 @@ import { } from '@kbn/core/server'; import { registerRoutes } from '@kbn/server-route-repository'; import { StreamsConfig, configSchema, exposeToBrowserConfig } from '../common/config'; -import { StreamsRouteRepository } from './routes'; -import { RouteDependencies } from './routes/types'; +import { streamsRouteRepository } from './routes'; import { StreamsPluginSetupDependencies, StreamsPluginStartDependencies, @@ -58,8 +57,8 @@ export class StreamsPlugin logger: this.logger, } as StreamsServer; - registerRoutes<RouteDependencies>({ - repository: StreamsRouteRepository, + registerRoutes({ + repository: streamsRouteRepository, dependencies: { server: this.server, getScopedClients: async ({ request }: { request: KibanaRequest }) => { diff --git a/x-pack/plugins/streams/server/routes/esql/route.ts b/x-pack/plugins/streams/server/routes/esql/route.ts new file mode 100644 index 0000000000000..0e0e41eee3c7e --- /dev/null +++ b/x-pack/plugins/streams/server/routes/esql/route.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 { excludeFrozenQuery } from '@kbn/observability-utils-common/es/queries/exclude_frozen_query'; +import { kqlQuery } from '@kbn/observability-utils-common/es/queries/kql_query'; +import { rangeQuery } from '@kbn/observability-utils-common/es/queries/range_query'; +import { + UnparsedEsqlResponse, + createObservabilityEsClient, +} from '@kbn/observability-utils-server/es/client/create_observability_es_client'; +import { z } from '@kbn/zod'; +import { isNumber } from 'lodash'; +import { createServerRoute } from '../create_server_route'; + +export const executeEsqlRoute = createServerRoute({ + endpoint: 'POST /internal/streams/esql', + params: z.object({ + body: z.object({ + query: z.string(), + operationName: z.string(), + filter: z.object({}).passthrough().optional(), + kuery: z.string().optional(), + start: z.number().optional(), + end: z.number().optional(), + }), + }), + handler: async ({ params, request, logger, getScopedClients }): Promise<UnparsedEsqlResponse> => { + const { scopedClusterClient } = await getScopedClients({ request }); + const observabilityEsClient = createObservabilityEsClient({ + client: scopedClusterClient.asCurrentUser, + logger, + plugin: 'streams', + }); + + const { + body: { operationName, query, filter, kuery, start, end }, + } = params; + + const response = await observabilityEsClient.esql( + operationName, + { + query, + filter: { + bool: { + filter: [ + filter || { match_all: {} }, + ...kqlQuery(kuery), + ...excludeFrozenQuery(), + ...(isNumber(start) && isNumber(end) ? rangeQuery(start, end) : []), + ], + }, + }, + }, + { transform: 'none' } + ); + + return response; + }, +}); + +export const esqlRoutes = { + ...executeEsqlRoute, +}; diff --git a/x-pack/plugins/streams/server/routes/index.ts b/x-pack/plugins/streams/server/routes/index.ts index 6fc734d3371b4..7267dbedeacff 100644 --- a/x-pack/plugins/streams/server/routes/index.ts +++ b/x-pack/plugins/streams/server/routes/index.ts @@ -5,15 +5,18 @@ * 2.0. */ +import { esqlRoutes } from './esql/route'; import { deleteStreamRoute } from './streams/delete'; +import { disableStreamsRoute } from './streams/disable'; import { editStreamRoute } from './streams/edit'; import { enableStreamsRoute } from './streams/enable'; import { forkStreamsRoute } from './streams/fork'; import { listStreamsRoute } from './streams/list'; import { readStreamRoute } from './streams/read'; import { resyncStreamsRoute } from './streams/resync'; +import { streamsStatusRoutes } from './streams/settings'; -export const StreamsRouteRepository = { +export const streamsRouteRepository = { ...enableStreamsRoute, ...resyncStreamsRoute, ...forkStreamsRoute, @@ -21,6 +24,9 @@ export const StreamsRouteRepository = { ...editStreamRoute, ...deleteStreamRoute, ...listStreamsRoute, + ...streamsStatusRoutes, + ...esqlRoutes, + ...disableStreamsRoute, }; -export type StreamsRouteRepository = typeof StreamsRouteRepository; +export type StreamsRouteRepository = typeof streamsRouteRepository; diff --git a/x-pack/plugins/streams/server/routes/streams/delete.ts b/x-pack/plugins/streams/server/routes/streams/delete.ts index 3820975dbe16a..a2092838792cf 100644 --- a/x-pack/plugins/streams/server/routes/streams/delete.ts +++ b/x-pack/plugins/streams/server/routes/streams/delete.ts @@ -8,6 +8,7 @@ import { z } from '@kbn/zod'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; +import { badRequest, internal, notFound } from '@hapi/boom'; import { DefinitionNotFound, ForkConditionMissing, @@ -20,18 +21,15 @@ import { MalformedStreamId } from '../../lib/streams/errors/malformed_stream_id' import { getParentId } from '../../lib/streams/helpers/hierarchy'; export const deleteStreamRoute = createServerRoute({ - endpoint: 'DELETE /api/streams/{id} 2023-10-31', + endpoint: 'DELETE /api/streams/{id}', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({ @@ -39,7 +37,13 @@ export const deleteStreamRoute = createServerRoute({ id: z.string(), }), }), - handler: async ({ response, params, logger, request, getScopedClients }) => { + handler: async ({ + response, + params, + logger, + request, + getScopedClients, + }): Promise<{ acknowledged: true }> => { try { const { scopedClusterClient } = await getScopedClients({ request }); @@ -48,14 +52,15 @@ export const deleteStreamRoute = createServerRoute({ throw new MalformedStreamId('Cannot delete root stream'); } + // need to update parent first to cut off documents streaming down await updateParentStream(scopedClusterClient, params.path.id, parentId, logger); await deleteStream(scopedClusterClient, params.path.id, logger); - return response.ok({ body: { acknowledged: true } }); + return { acknowledged: true }; } catch (e) { if (e instanceof IndexTemplateNotFound || e instanceof DefinitionNotFound) { - return response.notFound({ body: e }); + throw notFound(e); } if ( @@ -63,15 +68,19 @@ export const deleteStreamRoute = createServerRoute({ e instanceof ForkConditionMissing || e instanceof MalformedStreamId ) { - return response.customError({ body: e, statusCode: 400 }); + throw badRequest(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); -async function deleteStream(scopedClusterClient: IScopedClusterClient, id: string, logger: Logger) { +export async function deleteStream( + scopedClusterClient: IScopedClusterClient, + id: string, + logger: Logger +) { try { const { definition } = await readStream({ scopedClusterClient, id }); for (const child of definition.children) { diff --git a/x-pack/plugins/streams/server/routes/streams/disable.ts b/x-pack/plugins/streams/server/routes/streams/disable.ts new file mode 100644 index 0000000000000..b760b58f1fafd --- /dev/null +++ b/x-pack/plugins/streams/server/routes/streams/disable.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 { badRequest, internal } from '@hapi/boom'; +import { z } from '@kbn/zod'; +import { SecurityException } from '../../lib/streams/errors'; +import { createServerRoute } from '../create_server_route'; +import { deleteStream } from './delete'; + +export const disableStreamsRoute = createServerRoute({ + endpoint: 'POST /api/streams/_disable', + params: z.object({}), + options: { + access: 'internal', + }, + security: { + authz: { + requiredPrivileges: ['streams_write'], + }, + }, + handler: async ({ + request, + response, + logger, + getScopedClients, + }): Promise<{ acknowledged: true }> => { + try { + const { scopedClusterClient } = await getScopedClients({ request }); + + await deleteStream(scopedClusterClient, 'logs', logger); + + return { acknowledged: true }; + } catch (e) { + if (e instanceof SecurityException) { + throw badRequest(e); + } + throw internal(e); + } + }, +}); diff --git a/x-pack/plugins/streams/server/routes/streams/edit.ts b/x-pack/plugins/streams/server/routes/streams/edit.ts index b82b4d54044da..cda73907d2302 100644 --- a/x-pack/plugins/streams/server/routes/streams/edit.ts +++ b/x-pack/plugins/streams/server/routes/streams/edit.ts @@ -8,6 +8,7 @@ import { z } from '@kbn/zod'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; +import { badRequest, internal, notFound } from '@hapi/boom'; import { DefinitionNotFound, ForkConditionMissing, @@ -28,18 +29,15 @@ import { getParentId } from '../../lib/streams/helpers/hierarchy'; import { MalformedChildren } from '../../lib/streams/errors/malformed_children'; export const editStreamRoute = createServerRoute({ - endpoint: 'PUT /api/streams/{id} 2023-10-31', + endpoint: 'PUT /api/streams/{id}', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({ @@ -59,22 +57,10 @@ export const editStreamRoute = createServerRoute({ const parentId = getParentId(params.path.id); let parentDefinition: StreamDefinition | undefined; - if (parentId) { - parentDefinition = await updateParentStream( - scopedClusterClient, - parentId, - params.path.id, - logger - ); - } const streamDefinition = { ...params.body }; - await syncStream({ - scopedClusterClient, - definition: { ...streamDefinition, id: params.path.id }, - rootDefinition: parentDefinition, - logger, - }); + // always need to go from the leaves to the parent when syncing ingest pipelines, otherwise data + // will be routed before the data stream is ready for (const child of streamDefinition.children) { const streamExists = await checkStreamExists({ @@ -99,10 +85,26 @@ export const editStreamRoute = createServerRoute({ }); } - return response.ok({ body: { acknowledged: true } }); + await syncStream({ + scopedClusterClient, + definition: { ...streamDefinition, id: params.path.id }, + rootDefinition: parentDefinition, + logger, + }); + + if (parentId) { + parentDefinition = await updateParentStream( + scopedClusterClient, + parentId, + params.path.id, + logger + ); + } + + return { acknowledged: true }; } catch (e) { if (e instanceof IndexTemplateNotFound || e instanceof DefinitionNotFound) { - return response.notFound({ body: e }); + throw notFound(e); } if ( @@ -110,10 +112,10 @@ export const editStreamRoute = createServerRoute({ e instanceof ForkConditionMissing || e instanceof MalformedStreamId ) { - return response.customError({ body: e, statusCode: 400 }); + throw badRequest(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); diff --git a/x-pack/plugins/streams/server/routes/streams/enable.ts b/x-pack/plugins/streams/server/routes/streams/enable.ts index 27d8929b28e50..e163c6cbc8bb2 100644 --- a/x-pack/plugins/streams/server/routes/streams/enable.ts +++ b/x-pack/plugins/streams/server/routes/streams/enable.ts @@ -6,6 +6,7 @@ */ import { z } from '@kbn/zod'; +import { badRequest, internal } from '@hapi/boom'; import { SecurityException } from '../../lib/streams/errors'; import { createServerRoute } from '../create_server_route'; import { syncStream } from '../../lib/streams/stream_crud'; @@ -13,22 +14,24 @@ import { rootStreamDefinition } from '../../lib/streams/root_stream_definition'; import { createStreamsIndex } from '../../lib/streams/internal_stream_mapping'; export const enableStreamsRoute = createServerRoute({ - endpoint: 'POST /api/streams/_enable 2023-10-31', + endpoint: 'POST /api/streams/_enable', params: z.object({}), options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, - handler: async ({ request, response, logger, getScopedClients }) => { + handler: async ({ + request, + response, + logger, + getScopedClients, + }): Promise<{ acknowledged: true }> => { try { const { scopedClusterClient } = await getScopedClients({ request }); await createStreamsIndex(scopedClusterClient); @@ -37,12 +40,12 @@ export const enableStreamsRoute = createServerRoute({ definition: rootStreamDefinition, logger, }); - return response.ok({ body: { acknowledged: true } }); + return { acknowledged: true }; } catch (e) { if (e instanceof SecurityException) { - return response.customError({ body: e, statusCode: 400 }); + throw badRequest(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); diff --git a/x-pack/plugins/streams/server/routes/streams/fork.ts b/x-pack/plugins/streams/server/routes/streams/fork.ts index 44f4052878003..12dce248dcdd1 100644 --- a/x-pack/plugins/streams/server/routes/streams/fork.ts +++ b/x-pack/plugins/streams/server/routes/streams/fork.ts @@ -6,6 +6,7 @@ */ import { z } from '@kbn/zod'; +import { badRequest, internal, notFound } from '@hapi/boom'; import { DefinitionNotFound, ForkConditionMissing, @@ -19,18 +20,15 @@ import { MalformedStreamId } from '../../lib/streams/errors/malformed_stream_id' import { isChildOf } from '../../lib/streams/helpers/hierarchy'; export const forkStreamsRoute = createServerRoute({ - endpoint: 'POST /api/streams/{id}/_fork 2023-10-31', + endpoint: 'POST /api/streams/{id}/_fork', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({ @@ -39,7 +37,12 @@ export const forkStreamsRoute = createServerRoute({ }), body: z.object({ stream: streamDefinitonWithoutChildrenSchema, condition: conditionSchema }), }), - handler: async ({ response, params, logger, request, getScopedClients }) => { + handler: async ({ + params, + logger, + request, + getScopedClients, + }): Promise<{ acknowledged: true }> => { try { if (!params.body.condition) { throw new ForkConditionMissing('You must provide a condition to fork a stream'); @@ -73,29 +76,30 @@ export const forkStreamsRoute = createServerRoute({ params.body.stream.fields ); - rootDefinition.children.push({ - id: params.body.stream.id, - condition: params.body.condition, - }); - + // need to create the child first, otherwise we risk streaming data even though the child data stream is not ready await syncStream({ scopedClusterClient, - definition: rootDefinition, + definition: childDefinition, rootDefinition, logger, }); + rootDefinition.children.push({ + id: params.body.stream.id, + condition: params.body.condition, + }); + await syncStream({ scopedClusterClient, - definition: childDefinition, + definition: rootDefinition, rootDefinition, logger, }); - return response.ok({ body: { acknowledged: true } }); + return { acknowledged: true }; } catch (e) { if (e instanceof IndexTemplateNotFound || e instanceof DefinitionNotFound) { - return response.notFound({ body: e }); + throw notFound(e); } if ( @@ -103,10 +107,10 @@ export const forkStreamsRoute = createServerRoute({ e instanceof ForkConditionMissing || e instanceof MalformedStreamId ) { - return response.customError({ body: e, statusCode: 400 }); + throw badRequest(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); diff --git a/x-pack/plugins/streams/server/routes/streams/list.ts b/x-pack/plugins/streams/server/routes/streams/list.ts index 2e4f13a89bb41..d3b88ffc36a45 100644 --- a/x-pack/plugins/streams/server/routes/streams/list.ts +++ b/x-pack/plugins/streams/server/routes/streams/list.ts @@ -6,65 +6,71 @@ */ import { z } from '@kbn/zod'; +import { notFound, internal } from '@hapi/boom'; import { createServerRoute } from '../create_server_route'; import { DefinitionNotFound } from '../../lib/streams/errors'; import { listStreams } from '../../lib/streams/stream_crud'; +import { StreamDefinition } from '../../../common'; export const listStreamsRoute = createServerRoute({ - endpoint: 'GET /api/streams 2023-10-31', + endpoint: 'GET /api/streams', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({}), - handler: async ({ response, request, getScopedClients }) => { + handler: async ({ + response, + request, + getScopedClients, + }): Promise<{ definitions: StreamDefinition[]; trees: StreamTree[] }> => { try { const { scopedClusterClient } = await getScopedClients({ request }); - const definitions = await listStreams({ scopedClusterClient }); + const { definitions } = await listStreams({ scopedClusterClient }); const trees = asTrees(definitions); - return response.ok({ body: { streams: trees } }); + return { definitions, trees }; } catch (e) { if (e instanceof DefinitionNotFound) { - return response.notFound({ body: e }); + throw notFound(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); -interface ListStreamDefinition { +export interface StreamTree { id: string; - children: ListStreamDefinition[]; + children: StreamTree[]; } -function asTrees(definitions: Array<{ id: string[] }>) { - const trees: ListStreamDefinition[] = []; - definitions.forEach((definition) => { - const path = definition.id[0].split('.'); +function asTrees(definitions: Array<{ id: string }>) { + const trees: StreamTree[] = []; + const ids = definitions.map((definition) => definition.id); + + ids.sort((a, b) => a.split('.').length - b.split('.').length); + + ids.forEach((id) => { let currentTree = trees; - path.forEach((_id, index) => { - const partialPath = path.slice(0, index + 1).join('.'); - const existingNode = currentTree.find((node) => node.id === partialPath); - if (existingNode) { - currentTree = existingNode.children; - } else { - const newNode = { id: partialPath, children: [] }; - currentTree.push(newNode); - currentTree = newNode.children; - } - }); + let existingNode: StreamTree | undefined; + // traverse the tree following the prefix of the current id. + // once we reach the leaf, the current id is added as child - this works because the ids are sorted by depth + while ((existingNode = currentTree.find((node) => id.startsWith(node.id)))) { + currentTree = existingNode.children; + } + if (!existingNode) { + const newNode = { id, children: [] }; + currentTree.push(newNode); + } }); + return trees; } diff --git a/x-pack/plugins/streams/server/routes/streams/read.ts b/x-pack/plugins/streams/server/routes/streams/read.ts index 5ea2aaf5f2542..b9d21ef25b673 100644 --- a/x-pack/plugins/streams/server/routes/streams/read.ts +++ b/x-pack/plugins/streams/server/routes/streams/read.ts @@ -6,29 +6,38 @@ */ import { z } from '@kbn/zod'; +import { notFound, internal } from '@hapi/boom'; import { createServerRoute } from '../create_server_route'; import { DefinitionNotFound } from '../../lib/streams/errors'; import { readAncestors, readStream } from '../../lib/streams/stream_crud'; +import { StreamDefinition } from '../../../common'; export const readStreamRoute = createServerRoute({ - endpoint: 'GET /api/streams/{id} 2023-10-31', + endpoint: 'GET /api/streams/{id}', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({ path: z.object({ id: z.string() }), }), - handler: async ({ response, params, request, getScopedClients }) => { + handler: async ({ + response, + params, + request, + logger, + getScopedClients, + }): Promise< + StreamDefinition & { + inheritedFields: Array<StreamDefinition['fields'][number] & { from: string }>; + } + > => { try { const { scopedClusterClient } = await getScopedClients({ request }); const streamEntity = await readStream({ @@ -36,7 +45,7 @@ export const readStreamRoute = createServerRoute({ id: params.path.id, }); - const ancestors = await readAncestors({ + const { ancestors } = await readAncestors({ id: streamEntity.definition.id, scopedClusterClient, }); @@ -48,13 +57,13 @@ export const readStreamRoute = createServerRoute({ ), }; - return response.ok({ body }); + return body; } catch (e) { if (e instanceof DefinitionNotFound) { - return response.notFound({ body: e }); + throw notFound(e); } - return response.customError({ body: e, statusCode: 500 }); + throw internal(e); } }, }); diff --git a/x-pack/plugins/streams/server/routes/streams/resync.ts b/x-pack/plugins/streams/server/routes/streams/resync.ts index 2365252ab00e6..8e520410ca5c2 100644 --- a/x-pack/plugins/streams/server/routes/streams/resync.ts +++ b/x-pack/plugins/streams/server/routes/streams/resync.ts @@ -10,31 +10,34 @@ import { createServerRoute } from '../create_server_route'; import { syncStream, readStream, listStreams } from '../../lib/streams/stream_crud'; export const resyncStreamsRoute = createServerRoute({ - endpoint: 'POST /api/streams/_resync 2023-10-31', + endpoint: 'POST /api/streams/_resync', options: { - access: 'public', - availability: { - stability: 'experimental', - }, - security: { - authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', - }, + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', }, }, params: z.object({}), - handler: async ({ response, logger, request, getScopedClients }) => { + handler: async ({ + response, + logger, + request, + getScopedClients, + }): Promise<{ acknowledged: true }> => { const { scopedClusterClient } = await getScopedClients({ request }); - const streams = await listStreams({ scopedClusterClient }); + const { definitions: streams } = await listStreams({ scopedClusterClient }); for (const stream of streams) { const { definition } = await readStream({ scopedClusterClient, - id: stream.id[0], + id: stream.id, }); + await syncStream({ scopedClusterClient, definition, @@ -42,6 +45,6 @@ export const resyncStreamsRoute = createServerRoute({ }); } - return response.ok({}); + return { acknowledged: true }; }, }); diff --git a/x-pack/plugins/streams/server/routes/streams/settings.ts b/x-pack/plugins/streams/server/routes/streams/settings.ts new file mode 100644 index 0000000000000..6e133b3948dd0 --- /dev/null +++ b/x-pack/plugins/streams/server/routes/streams/settings.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 { STREAMS_INDEX } from '../../../common/constants'; +import { createServerRoute } from '../create_server_route'; + +export const getStreamsStatusRoute = createServerRoute({ + endpoint: 'GET /api/streams/_status', + options: { + access: 'internal', + }, + security: { + authz: { + requiredPrivileges: ['streams_read'], + }, + }, + handler: async ({ request, getScopedClients }): Promise<{ enabled: boolean }> => { + const { scopedClusterClient } = await getScopedClients({ request }); + + return { + enabled: await scopedClusterClient.asInternalUser.indices.exists({ + index: STREAMS_INDEX, + }), + }; + }, +}); + +export const streamsStatusRoutes = { + ...getStreamsStatusRoute, +}; diff --git a/x-pack/plugins/streams/tsconfig.json b/x-pack/plugins/streams/tsconfig.json index c2fde35f9ca22..3f863145f4d22 100644 --- a/x-pack/plugins/streams/tsconfig.json +++ b/x-pack/plugins/streams/tsconfig.json @@ -27,5 +27,8 @@ "@kbn/zod", "@kbn/encrypted-saved-objects-plugin", "@kbn/licensing-plugin", + "@kbn/server-route-repository-client", + "@kbn/observability-utils-server", + "@kbn/observability-utils-common" ] } diff --git a/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx b/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx new file mode 100644 index 0000000000000..1660042b2cb66 --- /dev/null +++ b/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.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 { coreMock } from '@kbn/core/public/mocks'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { StreamsPluginStart } from '@kbn/streams-plugin/public'; +import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; +import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana'; + +export function getMockStreamsAppContext(): StreamsAppKibanaContext { + const core = coreMock.createStart(); + + return { + core, + dependencies: { + start: { + observabilityShared: {} as unknown as ObservabilitySharedPluginStart, + dataViews: {} as unknown as DataViewsPublicPluginStart, + data: {} as unknown as DataPublicPluginStart, + unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart, + streams: {} as unknown as StreamsPluginStart, + share: {} as unknown as SharePublicStart, + }, + }, + services: { + query: jest.fn(), + }, + }; +} diff --git a/x-pack/plugins/streams_app/.storybook/jest_setup.js b/x-pack/plugins/streams_app/.storybook/jest_setup.js new file mode 100644 index 0000000000000..32071b8aa3f62 --- /dev/null +++ b/x-pack/plugins/streams_app/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/streams_app/.storybook/main.js b/x-pack/plugins/streams_app/.storybook/main.js new file mode 100644 index 0000000000000..86b48c32f103e --- /dev/null +++ b/x-pack/plugins/streams_app/.storybook/main.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/streams_app/.storybook/preview.js b/x-pack/plugins/streams_app/.storybook/preview.js new file mode 100644 index 0000000000000..c8155e9c3d92c --- /dev/null +++ b/x-pack/plugins/streams_app/.storybook/preview.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; +import * as jest from 'jest-mock'; + +window.jest = jest; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/streams_app/.storybook/storybook_decorator.tsx b/x-pack/plugins/streams_app/.storybook/storybook_decorator.tsx new file mode 100644 index 0000000000000..617b5aee8128f --- /dev/null +++ b/x-pack/plugins/streams_app/.storybook/storybook_decorator.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ComponentType, useMemo } from 'react'; +import { StreamsAppContextProvider } from '../public/components/streams_app_context_provider'; +import { getMockStreamsAppContext } from './get_mock_streams_app_context'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + const context = useMemo(() => getMockStreamsAppContext(), []); + return ( + <StreamsAppContextProvider context={context}> + <Story /> + </StreamsAppContextProvider> + ); +} diff --git a/x-pack/plugins/streams_app/README.md b/x-pack/plugins/streams_app/README.md new file mode 100644 index 0000000000000..6e03524f9274b --- /dev/null +++ b/x-pack/plugins/streams_app/README.md @@ -0,0 +1,3 @@ +# Streams app + +Home of the Streams app plugin, which allows users to manage Streams via the UI. diff --git a/x-pack/plugins/streams_app/common/entity_source_query.ts b/x-pack/plugins/streams_app/common/entity_source_query.ts new file mode 100644 index 0000000000000..c076d1f6bd87f --- /dev/null +++ b/x-pack/plugins/streams_app/common/entity_source_query.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 function entitySourceQuery({ entity }: { entity: Record<string, string> }) { + return { + bool: { + filter: Object.entries(entity).map(([key, value]) => ({ term: { [key]: value } })), + }, + }; +} diff --git a/x-pack/plugins/streams_app/common/index.ts b/x-pack/plugins/streams_app/common/index.ts new file mode 100644 index 0000000000000..c41a05b84d307 --- /dev/null +++ b/x-pack/plugins/streams_app/common/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { StreamDefinition } from '@kbn/streams-plugin/common'; + +interface EntityBase { + type: string; + displayName: string; + properties: Record<string, unknown>; +} + +export type StreamEntity = EntityBase & { type: 'stream'; properties: StreamDefinition }; + +export type Entity = StreamEntity; + +export interface EntityTypeDefinition { + displayName: string; +} diff --git a/x-pack/plugins/streams_app/jest.config.js b/x-pack/plugins/streams_app/jest.config.js new file mode 100644 index 0000000000000..d9c01c40a322d --- /dev/null +++ b/x-pack/plugins/streams_app/jest.config.js @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: [ + '<rootDir>/x-pack/plugins/streams_app/public', + '<rootDir>/x-pack/plugins/streams_app/common', + '<rootDir>/x-pack/plugins/streams_app/server', + ], + setupFiles: ['<rootDir>/x-pack/plugins/streams_app/.storybook/jest_setup.js'], + collectCoverage: true, + collectCoverageFrom: [ + '<rootDir>/x-pack/plugins/streams_app/{public,common,server}/**/*.{js,ts,tsx}', + ], + + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/streams_app/kibana.jsonc b/x-pack/plugins/streams_app/kibana.jsonc new file mode 100644 index 0000000000000..16666084c53e5 --- /dev/null +++ b/x-pack/plugins/streams_app/kibana.jsonc @@ -0,0 +1,26 @@ +{ + "type": "plugin", + "id": "@kbn/streams-app-plugin", + "owner": "@simianhacker @flash1293 @dgieselaar", + "group": "observability", + "visibility": "private", + "plugin": { + "id": "streamsApp", + "server": true, + "browser": true, + "configPath": ["xpack", "streamsApp"], + "requiredPlugins": [ + "streams", + "observabilityShared", + "data", + "dataViews", + "unifiedSearch", + "share" + ], + "requiredBundles": [ + "kibanaReact" + ], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/streams_app/public/application.tsx b/x-pack/plugins/streams_app/public/application.tsx new file mode 100644 index 0000000000000..720f785ecde4b --- /dev/null +++ b/x-pack/plugins/streams_app/public/application.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 ReactDOM from 'react-dom'; +import { APP_WRAPPER_CLASS, type AppMountParameters, type CoreStart } from '@kbn/core/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { css } from '@emotion/css'; +import type { StreamsAppStartDependencies } from './types'; +import { StreamsAppServices } from './services/types'; +import { AppRoot } from './components/app_root'; + +export const renderApp = ({ + coreStart, + pluginsStart, + services, + appMountParameters, +}: { + coreStart: CoreStart; + pluginsStart: StreamsAppStartDependencies; + services: StreamsAppServices; +} & { appMountParameters: AppMountParameters }) => { + const { element } = appMountParameters; + + const appWrapperClassName = css` + overflow: auto; + `; + const appWrapperElement = document.getElementsByClassName(APP_WRAPPER_CLASS)[1]; + appWrapperElement.classList.add(appWrapperClassName); + + ReactDOM.render( + <KibanaRenderContextProvider {...coreStart}> + <AppRoot + appMountParameters={appMountParameters} + coreStart={coreStart} + pluginsStart={pluginsStart} + services={services} + /> + </KibanaRenderContextProvider>, + element + ); + return () => { + ReactDOM.unmountComponentAtNode(element); + appWrapperElement.classList.remove(APP_WRAPPER_CLASS); + }; +}; diff --git a/x-pack/plugins/streams_app/public/components/app_root/index.tsx b/x-pack/plugins/streams_app/public/components/app_root/index.tsx new file mode 100644 index 0000000000000..c5e8b78ae2155 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/app_root/index.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import React from 'react'; +import { type AppMountParameters, type CoreStart } from '@kbn/core/public'; +import { + BreadcrumbsContextProvider, + RouteRenderer, + RouterProvider, +} from '@kbn/typed-react-router-config'; +import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { StreamsAppContextProvider } from '../streams_app_context_provider'; +import { streamsAppRouter } from '../../routes/config'; +import { StreamsAppStartDependencies } from '../../types'; +import { StreamsAppServices } from '../../services/types'; + +export function AppRoot({ + coreStart, + pluginsStart, + services, + appMountParameters, +}: { + coreStart: CoreStart; + pluginsStart: StreamsAppStartDependencies; + services: StreamsAppServices; +} & { appMountParameters: AppMountParameters }) { + const { history } = appMountParameters; + + const context = { + core: coreStart, + dependencies: { + start: pluginsStart, + }, + services, + }; + + return ( + <StreamsAppContextProvider context={context}> + <RedirectAppLinks coreStart={coreStart}> + <RouterProvider history={history} router={streamsAppRouter}> + <BreadcrumbsContextProvider> + <RouteRenderer /> + </BreadcrumbsContextProvider> + <StreamsAppHeaderActionMenu appMountParameters={appMountParameters} /> + </RouterProvider> + </RedirectAppLinks> + </StreamsAppContextProvider> + ); +} + +export function StreamsAppHeaderActionMenu({ + appMountParameters, +}: { + appMountParameters: AppMountParameters; +}) { + const { setHeaderActionMenu, theme$ } = appMountParameters; + + return ( + <HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}> + <EuiFlexGroup responsive={false} gutterSize="s"> + <EuiFlexItem> + <></> + </EuiFlexItem> + </EuiFlexGroup> + </HeaderMenuPortal> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx b/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx new file mode 100644 index 0000000000000..d2ce4859e66d1 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiIcon, EuiLink, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useStreamsAppBreadcrumbs } from '../../hooks/use_streams_app_breadcrumbs'; +import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; +import { EntityOverviewTabList } from '../entity_overview_tab_list'; +import { LoadingPanel } from '../loading_panel'; +import { StreamsAppPageBody } from '../streams_app_page_body'; +import { StreamsAppPageHeader } from '../streams_app_page_header'; +import { StreamsAppPageHeaderTitle } from '../streams_app_page_header/streams_app_page_header_title'; + +export interface EntityViewTab { + name: string; + label: string; + content: React.ReactElement; +} + +export function EntityDetailViewWithoutParams({ + selectedTab, + tabs, + entity, +}: { + selectedTab: string; + tabs: EntityViewTab[]; + entity: { + displayName?: string; + id: string; + }; +}) { + const router = useStreamsAppRouter(); + useStreamsAppBreadcrumbs(() => { + if (!entity.displayName) { + return []; + } + + return [ + { + title: entity.displayName, + path: `/{key}`, + params: { path: { key: entity.id } }, + } as const, + ]; + }, [entity.displayName, entity.id]); + + if (!entity.displayName) { + return <LoadingPanel />; + } + + const tabMap = Object.fromEntries( + tabs.map((tab) => { + return [ + tab.name, + { + href: router.link('/{key}/{tab}', { + path: { key: entity.id, tab: tab.name }, + }), + label: tab.label, + content: tab.content, + }, + ]; + }) + ); + + const selectedTabObject = tabMap[selectedTab]; + + return ( + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiPanel color="transparent"> + <EuiLink data-test-subj="streamsEntityDetailViewGoBackHref" href={router.link('/')}> + <EuiFlexGroup direction="row" alignItems="center" gutterSize="s"> + <EuiIcon type="arrowLeft" /> + {i18n.translate('xpack.streams.entityDetailView.goBackLinkLabel', { + defaultMessage: 'Back', + })} + </EuiFlexGroup> + </EuiLink> + </EuiPanel> + <StreamsAppPageHeader + verticalPaddingSize="none" + title={<StreamsAppPageHeaderTitle title={entity.displayName} />} + > + <EntityOverviewTabList + tabs={Object.entries(tabMap).map(([tabKey, { label, href }]) => { + return { + name: tabKey, + label, + href, + selected: selectedTab === tabKey, + }; + })} + /> + </StreamsAppPageHeader> + <StreamsAppPageBody>{selectedTabObject.content}</StreamsAppPageBody> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/entity_detail_view_header_section/index.tsx b/x-pack/plugins/streams_app/public/components/entity_detail_view_header_section/index.tsx new file mode 100644 index 0000000000000..4aafb7a7e9bc6 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/entity_detail_view_header_section/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import React from 'react'; + +export function EntityDetailViewHeaderSection({ + title, + children, +}: { + title: React.ReactNode; + children: React.ReactNode; +}) { + return ( + <EuiFlexGroup direction="column" gutterSize="s"> + <EuiText + className={css` + font-weight: 600; + `} + > + {title} + </EuiText> + {children} + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/entity_overview_tab_list/index.tsx b/x-pack/plugins/streams_app/public/components/entity_overview_tab_list/index.tsx new file mode 100644 index 0000000000000..08502b26f7ca3 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/entity_overview_tab_list/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export function EntityOverviewTabList< + T extends { name: string; label: string; href: string; selected: boolean } +>({ tabs }: { tabs: T[] }) { + const theme = useEuiTheme().euiTheme; + + return ( + <EuiTabs + size="m" + className={css` + padding: 0 ${theme.size.l}; + `} + > + {tabs.map((tab) => { + return ( + <EuiTab key={tab.name} href={tab.href} isSelected={tab.selected}> + {tab.label} + </EuiTab> + ); + })} + </EuiTabs> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/esql_chart/controlled_esql_chart.tsx b/x-pack/plugins/streams_app/public/components/esql_chart/controlled_esql_chart.tsx new file mode 100644 index 0000000000000..9008f8ee47098 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/esql_chart/controlled_esql_chart.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { + AreaSeries, + Axis, + BarSeries, + Chart, + CurveType, + LineSeries, + Position, + ScaleType, + Settings, + Tooltip, + niceTimeFormatter, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getTimeZone } from '@kbn/observability-utils-browser/utils/ui_settings/get_timezone'; +import { css } from '@emotion/css'; +import { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async'; +import type { UnparsedEsqlResponse } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; +import { esqlResultToTimeseries } from '../../util/esql_result_to_timeseries'; +import { useKibana } from '../../hooks/use_kibana'; +import { LoadingPanel } from '../loading_panel'; + +const END_ZONE_LABEL = i18n.translate('xpack.streams.esqlChart.endzone', { + defaultMessage: + 'The selected time range does not include this entire bucket. It might contain partial data.', +}); + +function getChartType(type: 'area' | 'bar' | 'line') { + switch (type) { + case 'area': + return AreaSeries; + case 'bar': + return BarSeries; + default: + return LineSeries; + } +} + +export function ControlledEsqlChart<T extends string>({ + id, + result, + metricNames, + chartType = 'line', + height, +}: { + id: string; + result: AbortableAsyncState<UnparsedEsqlResponse>; + metricNames: T[]; + chartType?: 'area' | 'bar' | 'line'; + height: number; +}) { + const { + core: { uiSettings }, + } = useKibana(); + + const allTimeseries = useMemo( + () => + esqlResultToTimeseries<T>({ + result, + metricNames, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [result, ...metricNames] + ); + + if (result.loading && !result.value?.values.length) { + return ( + <LoadingPanel + loading + className={css` + height: ${height}px; + `} + /> + ); + } + + const xValues = allTimeseries.flatMap(({ data }) => data.map(({ x }) => x)); + + const min = Math.min(...xValues); + const max = Math.max(...xValues); + + const isEmpty = min === 0 && max === 0; + + const xFormatter = niceTimeFormatter([min, max]); + + const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max }; + + const yTickFormat = (value: number | null) => (value === null ? '' : String(value)); + const yLabelFormat = (label: string) => label; + + const timeZone = getTimeZone(uiSettings); + + return ( + <Chart + id={id} + className={css` + height: ${height}px; + `} + > + <Tooltip + stickTo="top" + showNullValues={false} + headerFormatter={({ value }) => { + const formattedValue = xFormatter(value); + if (max === value) { + return ( + <> + <EuiFlexGroup + alignItems="center" + responsive={false} + gutterSize="xs" + style={{ fontWeight: 'normal' }} + > + <EuiFlexItem grow={false}> + <EuiIcon type="iInCircle" /> + </EuiFlexItem> + <EuiFlexItem>{END_ZONE_LABEL}</EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xs" /> + {formattedValue} + </> + ); + } + return formattedValue; + }} + /> + <Settings + showLegend + legendPosition={Position.Bottom} + xDomain={xDomain} + locale={i18n.getLocale()} + /> + <Axis + id="x-axis" + position={Position.Bottom} + showOverlappingTicks + tickFormat={xFormatter} + gridLine={{ visible: false }} + /> + <Axis + id="y-axis" + ticks={3} + position={Position.Left} + tickFormat={yTickFormat} + labelFormat={yLabelFormat} + /> + {allTimeseries.map((serie) => { + const Series = getChartType(chartType); + + return ( + <Series + timeZone={timeZone} + key={serie.id} + id={serie.id} + xScaleType={ScaleType.Time} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={serie.metricNames} + data={serie.data} + curve={CurveType.CURVE_MONOTONE_X} + /> + ); + })} + </Chart> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/loading_panel/index.tsx b/x-pack/plugins/streams_app/public/components/loading_panel/index.tsx new file mode 100644 index 0000000000000..58e432b84a4bd --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/loading_panel/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import React from 'react'; + +export function LoadingPanel({ + loading = true, + size, + className, +}: { + loading?: boolean; + size?: React.ComponentProps<typeof EuiLoadingSpinner>['size']; + className?: string; +}) { + if (!loading) { + return null; + } + + return ( + <EuiFlexGroup + className={className} + alignItems="center" + justifyContent="center" + gutterSize="none" + > + <EuiSpacer size="xl" /> + <EuiLoadingSpinner size={size} /> + <EuiSpacer size="xl" /> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/not_found/index.tsx b/x-pack/plugins/streams_app/public/components/not_found/index.tsx new file mode 100644 index 0000000000000..c0c1d50000cfc --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/not_found/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +export function NotFound() { + return ( + <EuiCallOut + color="danger" + title={i18n.translate('xpack.streams.notFound.callOutTitle', { + defaultMessage: 'Page not found', + })} + > + {i18n.translate('xpack.streams.notFound.calloutLabel', { + defaultMessage: 'The current page can not be found.', + })} + </EuiCallOut> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/redirect_to/index.tsx b/x-pack/plugins/streams_app/public/components/redirect_to/index.tsx new file mode 100644 index 0000000000000..2bde67faa3b98 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/redirect_to/index.tsx @@ -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 React, { useLayoutEffect } from 'react'; +import { PathsOf, TypeOf } from '@kbn/typed-react-router-config'; +import { DeepPartial } from 'utility-types'; +import { merge } from 'lodash'; +import { StreamsAppRoutes } from '../../routes/config'; +import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; +import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; + +export function RedirectTo< + TPath extends PathsOf<StreamsAppRoutes>, + TParams extends TypeOf<StreamsAppRoutes, TPath, false> +>({ path, params }: { path: TPath; params?: DeepPartial<TParams> }) { + const router = useStreamsAppRouter(); + const currentParams = useStreamsAppParams('/*'); + useLayoutEffect(() => { + router.replace(path, ...([merge({}, currentParams, params)] as any)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return <></>; +} diff --git a/x-pack/plugins/streams_app/public/components/stream_detail_overview/index.tsx b/x-pack/plugins/streams_app/public/components/stream_detail_overview/index.tsx new file mode 100644 index 0000000000000..93a573fd4c01f --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/stream_detail_overview/index.tsx @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, EuiPanel } from '@elastic/eui'; +import { calculateAuto } from '@kbn/calculate-auto'; +import { i18n } from '@kbn/i18n'; +import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range'; +import { StreamDefinition } from '@kbn/streams-plugin/common'; +import moment from 'moment'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { ControlledEsqlChart } from '../esql_chart/controlled_esql_chart'; +import { StreamsAppSearchBar } from '../streams_app_search_bar'; + +export function StreamDetailOverview({ definition }: { definition?: StreamDefinition }) { + const { + dependencies: { + start: { + data, + dataViews, + streams: { streamsRepositoryClient }, + share, + }, + }, + } = useKibana(); + + const { + timeRange, + absoluteTimeRange: { start, end }, + setTimeRange, + } = useDateRange({ data }); + + const indexPatterns = useMemo(() => { + if (!definition?.id) { + return undefined; + } + + const isRoot = definition.id.indexOf('.') === -1; + + const dataStreamOfDefinition = definition.id; + + return isRoot + ? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`] + : [`${dataStreamOfDefinition}*`]; + }, [definition?.id]); + + const discoverLocator = useMemo( + () => share.url.locators.get('DISCOVER_APP_LOCATOR'), + [share.url.locators] + ); + + const queries = useMemo(() => { + if (!indexPatterns) { + return undefined; + } + + const baseQuery = `FROM ${indexPatterns.join(', ')}`; + + const bucketSize = Math.round( + calculateAuto.atLeast(50, moment.duration(1, 'minute'))!.asSeconds() + ); + + const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize} seconds)`; + + return { + baseQuery, + histogramQuery, + }; + }, [indexPatterns]); + + const discoverLink = useMemo(() => { + if (!discoverLocator || !queries?.baseQuery) { + return undefined; + } + + return discoverLocator.getRedirectUrl({ + query: { + esql: queries.baseQuery, + }, + }); + }, [queries?.baseQuery, discoverLocator]); + + const histogramQueryFetch = useStreamsAppFetch( + async ({ signal }) => { + if (!queries?.histogramQuery || !indexPatterns) { + return undefined; + } + + const existingIndices = await dataViews.getExistingIndices(indexPatterns); + + if (existingIndices.length === 0) { + return undefined; + } + + return streamsRepositoryClient.fetch('POST /internal/streams/esql', { + params: { + body: { + operationName: 'get_histogram_for_stream', + query: queries.histogramQuery, + start, + end, + }, + }, + signal, + }); + }, + [indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end] + ); + + return ( + <> + <EuiFlexGroup direction="column"> + <EuiFlexGroup direction="row" gutterSize="s"> + <EuiFlexItem grow> + <StreamsAppSearchBar + onQuerySubmit={({ dateRange }, isUpdate) => { + if (!isUpdate) { + histogramQueryFetch.refresh(); + return; + } + + if (dateRange) { + setTimeRange({ from: dateRange.from, to: dateRange?.to, mode: dateRange.mode }); + } + }} + onRefresh={() => { + histogramQueryFetch.refresh(); + }} + placeholder={i18n.translate( + 'xpack.streams.entityDetailOverview.searchBarPlaceholder', + { + defaultMessage: 'Filter data by using KQL', + } + )} + dateRangeFrom={timeRange.from} + dateRangeTo={timeRange.to} + /> + </EuiFlexItem> + <EuiButton + data-test-subj="streamsDetailOverviewOpenInDiscoverButton" + iconType="discoverApp" + href={discoverLink} + color="text" + > + {i18n.translate('xpack.streams.streamDetailOverview.openInDiscoverButtonLabel', { + defaultMessage: 'Open in Discover', + })} + </EuiButton> + </EuiFlexGroup> + <EuiPanel hasShadow={false} hasBorder> + <EuiFlexGroup direction="column"> + <ControlledEsqlChart + result={histogramQueryFetch} + id="entity_log_rate" + metricNames={['metric']} + height={200} + chartType={'bar'} + /> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexGroup> + </> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx b/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx new file mode 100644 index 0000000000000..ebf72a58d32a8 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EntityDetailViewWithoutParams, EntityViewTab } from '../entity_detail_view'; +import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { useKibana } from '../../hooks/use_kibana'; +import { StreamDetailOverview } from '../stream_detail_overview'; + +export function StreamDetailView() { + const { + path: { key, tab }, + } = useStreamsAppParams('/{key}/{tab}'); + + const { + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + + const { value: streamEntity } = useStreamsAppFetch( + ({ signal }) => { + return streamsRepositoryClient.fetch('GET /api/streams/{id}', { + signal, + params: { + path: { + id: key, + }, + }, + }); + }, + [streamsRepositoryClient, key] + ); + + const entity = { + id: key, + displayName: key, + }; + + const tabs: EntityViewTab[] = [ + { + name: 'overview', + content: <StreamDetailOverview definition={streamEntity} />, + label: i18n.translate('xpack.streams.streamDetailView.overviewTab', { + defaultMessage: 'Overview', + }), + }, + { + name: 'management', + content: <></>, + label: i18n.translate('xpack.streams.streamDetailView.managementTab', { + defaultMessage: 'Management', + }), + }, + ]; + + return <EntityDetailViewWithoutParams tabs={tabs} entity={entity} selectedTab={tab} />; +} diff --git a/x-pack/plugins/streams_app/public/components/stream_list_view/index.tsx b/x-pack/plugins/streams_app/public/components/stream_list_view/index.tsx new file mode 100644 index 0000000000000..e0530fc6bf5f0 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/stream_list_view/index.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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiSearchBar } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { StreamsAppPageHeader } from '../streams_app_page_header'; +import { StreamsAppPageHeaderTitle } from '../streams_app_page_header/streams_app_page_header_title'; +import { StreamsAppPageBody } from '../streams_app_page_body'; +import { StreamsTable } from '../streams_table'; + +export function StreamListView() { + const { + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + + const [query, setQuery] = useState(''); + + const streamsListFetch = useStreamsAppFetch( + ({ signal }) => { + return streamsRepositoryClient.fetch('GET /api/streams', { + signal, + }); + }, + [streamsRepositoryClient] + ); + + return ( + <EuiFlexGroup direction="column" gutterSize="none"> + <StreamsAppPageHeader + title={ + <StreamsAppPageHeaderTitle + title={i18n.translate('xpack.streams.streamsListViewPageHeaderTitle', { + defaultMessage: 'Streams', + })} + /> + } + /> + <StreamsAppPageBody> + <EuiFlexGroup direction="column"> + <EuiSearchBar + query={query} + box={{ + incremental: true, + }} + onChange={(nextQuery) => { + setQuery(nextQuery.queryText); + }} + /> + <StreamsTable listFetch={streamsListFetch} query={query} /> + </EuiFlexGroup> + </StreamsAppPageBody> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_context_provider/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_context_provider/index.tsx new file mode 100644 index 0000000000000..fd5886c089396 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_context_provider/index.tsx @@ -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 React, { useMemo } from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { StreamsAppKibanaContext } from '../../hooks/use_kibana'; + +export function StreamsAppContextProvider({ + context, + children, +}: { + context: StreamsAppKibanaContext; + children: React.ReactNode; +}) { + const servicesForContext = useMemo(() => { + const { core, ...services } = context; + return { + ...core, + ...services, + }; + }, [context]); + + return <KibanaContextProvider services={servicesForContext}>{children}</KibanaContextProvider>; +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx new file mode 100644 index 0000000000000..0f13dc31e277b --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.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 { EuiPanel, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export function StreamsAppPageBody({ children }: { children: React.ReactNode }) { + const theme = useEuiTheme().euiTheme; + return ( + <EuiPanel + hasBorder={false} + hasShadow={false} + className={css` + border-top: 1px solid ${theme.colors.lightShade}; + border-radius: 0px; + `} + paddingSize="l" + > + {children} + </EuiPanel> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_header/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_header/index.tsx new file mode 100644 index 0000000000000..1171772116f2a --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_header/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiPageHeader, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import React from 'react'; + +export function StreamsAppPageHeader({ + title, + children, + verticalPaddingSize = 'l', +}: { + title: React.ReactNode; + children?: React.ReactNode; + verticalPaddingSize?: 'none' | 'l'; +}) { + const theme = useEuiTheme().euiTheme; + + return ( + <EuiFlexGroup direction="column" gutterSize="s"> + <EuiPageHeader + className={css` + padding: ${verticalPaddingSize === 'none' ? 0 : theme.size[verticalPaddingSize]} + ${theme.size.l}; + `} + > + {title} + </EuiPageHeader> + {children} + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_header/streams_app_page_header_title.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_header/streams_app_page_header_title.tsx new file mode 100644 index 0000000000000..ff7d6581dea4f --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_header/streams_app_page_header_title.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiTitle } from '@elastic/eui'; +import React from 'react'; + +export function StreamsAppPageHeaderTitle({ title }: { title: string }) { + return ( + <EuiTitle size="l"> + <h1>{title}</h1> + </EuiTitle> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx new file mode 100644 index 0000000000000..c474f54c22745 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx @@ -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 { css } from '@emotion/css'; +import React from 'react'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; + +export function StreamsAppPageTemplate({ children }: { children: React.ReactNode }) { + const { + dependencies: { + start: { observabilityShared }, + }, + } = useKibana(); + + const { PageTemplate } = observabilityShared.navigation; + + return ( + <PageTemplate + pageSectionProps={{ + className: css` + max-height: calc(100vh - var(--euiFixedHeadersOffset, 0)); + overflow: auto; + padding-inline: 0px; + `, + contentProps: { + className: css` + padding-block: 0px; + display: flex; + height: 100%; + `, + }, + }} + > + <EuiPanel paddingSize="none" color="subdued" hasShadow={false} hasBorder={false}> + <EuiSpacer size="m" /> + {children} + </EuiPanel> + </PageTemplate> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_router_breadcrumb/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_router_breadcrumb/index.tsx new file mode 100644 index 0000000000000..88aab4662de61 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_router_breadcrumb/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createRouterBreadcrumbComponent } from '@kbn/typed-react-router-config'; +import type { StreamsAppRoutes } from '../../routes/config'; + +export const StreamsAppRouterBreadcrumb = createRouterBreadcrumbComponent<StreamsAppRoutes>(); diff --git a/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx new file mode 100644 index 0000000000000..563fb752efbd5 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { css } from '@emotion/css'; +import type { TimeRange } from '@kbn/es-query'; +import { SearchBar } from '@kbn/unified-search-plugin/public'; +import React, { useMemo } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { useKibana } from '../../hooks/use_kibana'; + +const parentClassName = css` + width: 100%; +`; + +interface Props { + query?: string; + dateRangeFrom?: string; + dateRangeTo?: string; + onQueryChange?: (payload: { dateRange?: TimeRange; query: string }) => void; + onQuerySubmit?: (payload: { dateRange?: TimeRange; query: string }, isUpdate?: boolean) => void; + onRefresh?: Required<React.ComponentProps<typeof SearchBar>>['onRefresh']; + placeholder?: string; + dataViews?: DataView[]; +} + +export function StreamsAppSearchBar({ + dateRangeFrom, + dateRangeTo, + onQueryChange, + onQuerySubmit, + onRefresh, + query, + placeholder, + dataViews, +}: Props) { + const { + dependencies: { + start: { unifiedSearch }, + }, + } = useKibana(); + + const queryObj = useMemo(() => (query ? { query, language: 'kuery' } : undefined), [query]); + + const showQueryInput = query === undefined; + + return ( + <div className={parentClassName}> + <unifiedSearch.ui.SearchBar + appName="streamsApp" + onQuerySubmit={({ dateRange, query: nextQuery }, isUpdate) => { + onQuerySubmit?.( + { dateRange, query: (nextQuery?.query as string | undefined) ?? '' }, + isUpdate + ); + }} + onQueryChange={({ dateRange, query: nextQuery }) => { + onQueryChange?.({ dateRange, query: (nextQuery?.query as string | undefined) ?? '' }); + }} + query={queryObj} + showQueryInput={showQueryInput} + showFilterBar={false} + showQueryMenu={false} + showDatePicker={Boolean(dateRangeFrom && dateRangeTo)} + showSubmitButton={true} + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + onRefresh={onRefresh} + displayStyle="inPage" + disableQueryLanguageSwitcher + placeholder={placeholder} + indexPatterns={dataViews} + /> + </div> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/streams_table/index.tsx b/x-pack/plugins/streams_app/public/components/streams_table/index.tsx new file mode 100644 index 0000000000000..f92c94f115e9b --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/streams_table/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiFlexGroup, + EuiIcon, + EuiLink, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async'; +import { StreamDefinition } from '@kbn/streams-plugin/common'; +import React, { useMemo } from 'react'; +import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; + +export function StreamsTable({ + listFetch, + query, +}: { + listFetch: AbortableAsyncState<{ definitions: StreamDefinition[] }>; + query: string; +}) { + const router = useStreamsAppRouter(); + + const items = useMemo(() => { + return listFetch.value?.definitions ?? []; + }, [listFetch.value?.definitions]); + + const filteredItems = useMemo(() => { + if (!query) { + return items; + } + + return items.filter((item) => item.id.toLowerCase().includes(query.toLowerCase())); + }, [query, items]); + + const columns = useMemo<Array<EuiBasicTableColumn<StreamDefinition>>>(() => { + return [ + { + field: 'id', + name: i18n.translate('xpack.streams.streamsTable.nameColumnTitle', { + defaultMessage: 'Name', + }), + render: (_, { id }) => { + return ( + <EuiFlexGroup direction="row" gutterSize="s" alignItems="center"> + <EuiIcon type="branch" /> + <EuiLink + data-test-subj="logsaiColumnsLink" + href={router.link('/{key}', { path: { key: id } })} + > + {id} + </EuiLink> + </EuiFlexGroup> + ); + }, + }, + ]; + }, [router]); + + return ( + <EuiFlexGroup direction="column" gutterSize="m"> + <EuiTitle size="xxs"> + <h2> + {i18n.translate('xpack.streams.streamsTable.tableTitle', { + defaultMessage: 'Streams', + })} + </h2> + </EuiTitle> + <EuiBasicTable columns={columns} items={filteredItems} loading={listFetch.loading} /> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/streams_app/public/hooks/use_kibana.tsx b/x-pack/plugins/streams_app/public/hooks/use_kibana.tsx new file mode 100644 index 0000000000000..9c6b23465fb11 --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_kibana.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 type { CoreStart } from '@kbn/core/public'; +import { useMemo } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { StreamsAppStartDependencies } from '../types'; +import type { StreamsAppServices } from '../services/types'; + +export interface StreamsAppKibanaContext { + core: CoreStart; + dependencies: { + start: StreamsAppStartDependencies; + }; + services: StreamsAppServices; +} + +const useTypedKibana = (): StreamsAppKibanaContext => { + const context = useKibana<CoreStart & Omit<StreamsAppKibanaContext, 'core'>>(); + + return useMemo(() => { + const { dependencies, services, ...core } = context.services; + + return { + core, + dependencies, + services, + }; + }, [context.services]); +}; + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/streams_app/public/hooks/use_streams_app_breadcrumbs.ts b/x-pack/plugins/streams_app/public/hooks/use_streams_app_breadcrumbs.ts new file mode 100644 index 0000000000000..e3ac760e3b779 --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_streams_app_breadcrumbs.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createUseBreadcrumbs } from '@kbn/typed-react-router-config'; +import { StreamsAppRoutes } from '../routes/config'; + +export const useStreamsAppBreadcrumbs = createUseBreadcrumbs<StreamsAppRoutes>(); diff --git a/x-pack/plugins/streams_app/public/hooks/use_streams_app_fetch.ts b/x-pack/plugins/streams_app/public/hooks/use_streams_app_fetch.ts new file mode 100644 index 0000000000000..08b112d4f207a --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_streams_app_fetch.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { + UseAbortableAsync, + useAbortableAsync, +} from '@kbn/observability-utils-browser/hooks/use_abortable_async'; +import { omit } from 'lodash'; +import { useKibana } from './use_kibana'; + +export const useStreamsAppFetch: UseAbortableAsync<{}, { disableToastOnError?: boolean }> = ( + callback, + deps, + options +) => { + const { + core: { notifications }, + } = useKibana(); + + const onError = (error: Error) => { + let requestUrl: string | undefined; + + if (!options?.disableToastOnError) { + if ( + 'body' in error && + typeof error.body === 'object' && + !!error.body && + 'message' in error.body && + typeof error.body.message === 'string' + ) { + error.message = error.body.message; + } + + if ( + 'request' in error && + typeof error.request === 'object' && + !!error.request && + 'url' in error.request && + typeof error.request.url === 'string' + ) { + requestUrl = error.request.url; + } + + notifications.toasts.addError(error, { + title: i18n.translate('xpack.streams.failedToFetchError', { + defaultMessage: 'Failed to fetch data{requestUrlSuffix}', + values: { + requestUrlSuffix: requestUrl ? ` (${requestUrl})` : '', + }, + }), + }); + } + }; + + const optionsForHook = { + ...omit(options, 'disableToastOnError'), + onError, + }; + + return useAbortableAsync( + ({ signal }) => { + return callback({ signal }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + deps, + optionsForHook + ); +}; diff --git a/x-pack/plugins/streams_app/public/hooks/use_streams_app_params.ts b/x-pack/plugins/streams_app/public/hooks/use_streams_app_params.ts new file mode 100644 index 0000000000000..2931a6fa64f8b --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_streams_app_params.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { type PathsOf, type TypeOf, useParams } from '@kbn/typed-react-router-config'; +import type { StreamsAppRoutes } from '../routes/config'; + +export function useStreamsAppParams<TPath extends PathsOf<StreamsAppRoutes>>( + path: TPath +): TypeOf<StreamsAppRoutes, TPath> { + return useParams(path)! as TypeOf<StreamsAppRoutes, TPath>; +} diff --git a/x-pack/plugins/streams_app/public/hooks/use_streams_app_route_path.ts b/x-pack/plugins/streams_app/public/hooks/use_streams_app_route_path.ts new file mode 100644 index 0000000000000..78e63ead57da6 --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_streams_app_route_path.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PathsOf, useRoutePath } from '@kbn/typed-react-router-config'; +import type { StreamsAppRoutes } from '../routes/config'; + +export function useStreamsAppRoutePath() { + const path = useRoutePath(); + + return path as PathsOf<StreamsAppRoutes>; +} diff --git a/x-pack/plugins/streams_app/public/hooks/use_streams_app_router.ts b/x-pack/plugins/streams_app/public/hooks/use_streams_app_router.ts new file mode 100644 index 0000000000000..17472044b7b4d --- /dev/null +++ b/x-pack/plugins/streams_app/public/hooks/use_streams_app_router.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 { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config'; +import { useMemo } from 'react'; +import type { StreamsAppRouter, StreamsAppRoutes } from '../routes/config'; +import { streamsAppRouter } from '../routes/config'; +import { useKibana } from './use_kibana'; + +interface StatefulStreamsAppRouter extends StreamsAppRouter { + push<T extends PathsOf<StreamsAppRoutes>>( + path: T, + ...params: TypeAsArgs<TypeOf<StreamsAppRoutes, T>> + ): void; + replace<T extends PathsOf<StreamsAppRoutes>>( + path: T, + ...params: TypeAsArgs<TypeOf<StreamsAppRoutes, T>> + ): void; +} + +export function useStreamsAppRouter(): StatefulStreamsAppRouter { + const { + core: { + http, + application: { navigateToApp }, + }, + } = useKibana(); + + const link = (...args: any[]) => { + // @ts-expect-error + return streamsAppRouter.link(...args); + }; + + return useMemo<StatefulStreamsAppRouter>( + () => ({ + ...streamsAppRouter, + push: (...args) => { + const next = link(...args); + navigateToApp('streams', { path: next, replace: false }); + }, + replace: (path, ...args) => { + const next = link(path, ...args); + navigateToApp('streams', { path: next, replace: true }); + }, + link: (path, ...args) => { + return http.basePath.prepend('/app/streams' + link(path, ...args)); + }, + }), + [navigateToApp, http.basePath] + ); +} diff --git a/x-pack/plugins/streams_app/public/index.ts b/x-pack/plugins/streams_app/public/index.ts new file mode 100644 index 0000000000000..eea2d8b7452a8 --- /dev/null +++ b/x-pack/plugins/streams_app/public/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; + +import { StreamsAppPlugin } from './plugin'; +import type { + StreamsAppPublicSetup, + StreamsAppPublicStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies, + ConfigSchema, +} from './types'; + +export type { StreamsAppPublicSetup, StreamsAppPublicStart }; + +export const plugin: PluginInitializer< + StreamsAppPublicSetup, + StreamsAppPublicStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies +> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => + new StreamsAppPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/streams_app/public/plugin.ts b/x-pack/plugins/streams_app/public/plugin.ts new file mode 100644 index 0000000000000..9df399693d02c --- /dev/null +++ b/x-pack/plugins/streams_app/public/plugin.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 { i18n } from '@kbn/i18n'; +import { map } from 'rxjs'; +import { + AppMountParameters, + AppUpdater, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, + PluginInitializerContext, +} from '@kbn/core/public'; +import type { Logger } from '@kbn/logging'; +import { STREAMS_APP_ID } from '@kbn/deeplinks-observability/constants'; +import type { + ConfigSchema, + StreamsAppPublicSetup, + StreamsAppPublicStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies, +} from './types'; +import { StreamsAppServices } from './services/types'; + +export class StreamsAppPlugin + implements + Plugin< + StreamsAppPublicSetup, + StreamsAppPublicStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext<ConfigSchema>) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup<StreamsAppStartDependencies, StreamsAppPublicStart>, + pluginsSetup: StreamsAppSetupDependencies + ): StreamsAppPublicSetup { + pluginsSetup.observabilityShared.navigation.registerSections( + pluginsSetup.streams.status$.pipe( + map(({ status }) => { + if (status !== 'enabled') { + return []; + } + + return [ + { + label: '', + sortKey: 101, + entries: [ + { + label: i18n.translate('xpack.streams.streamsAppLinkTitle', { + defaultMessage: 'Streams', + }), + app: STREAMS_APP_ID, + path: '/', + isTechnicalPreview: true, + matchPath(currentPath: string) { + return ['/', ''].some((testPath) => currentPath.startsWith(testPath)); + }, + }, + ], + }, + ]; + }) + ) + ); + + coreSetup.application.register({ + id: STREAMS_APP_ID, + title: i18n.translate('xpack.streams.appTitle', { + defaultMessage: 'Streams', + }), + euiIconType: 'logoObservability', + appRoute: '/app/streams', + category: DEFAULT_APP_CATEGORIES.observability, + order: 8001, + updater$: pluginsSetup.streams.status$.pipe( + map(({ status }): AppUpdater => { + return (app) => { + if (status !== 'enabled') { + return { + visibleIn: [], + deepLinks: [], + }; + } + + return { + visibleIn: ['sideNav', 'globalSearch'], + deepLinks: + status === 'enabled' + ? [ + { + id: 'streams', + title: i18n.translate('xpack.streams.streamsAppDeepLinkTitle', { + defaultMessage: 'Streams', + }), + path: '/', + }, + ] + : [], + }; + }; + }) + ), + mount: async (appMountParameters: AppMountParameters<unknown>) => { + // Load application bundle and Get start services + const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ + import('./application'), + coreSetup.getStartServices(), + ]); + + const services: StreamsAppServices = {}; + + return renderApp({ + coreStart, + pluginsStart, + services, + appMountParameters, + }); + }, + }); + + return {}; + } + + start(coreStart: CoreStart, pluginsStart: StreamsAppStartDependencies): StreamsAppPublicStart { + return {}; + } +} diff --git a/x-pack/plugins/streams_app/public/routes/config.tsx b/x-pack/plugins/streams_app/public/routes/config.tsx new file mode 100644 index 0000000000000..e3efdc6d871e7 --- /dev/null +++ b/x-pack/plugins/streams_app/public/routes/config.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { createRouter, Outlet, RouteMap } from '@kbn/typed-react-router-config'; +import * as t from 'io-ts'; +import React from 'react'; +import { StreamDetailView } from '../components/stream_detail_view'; +import { StreamsAppPageTemplate } from '../components/streams_app_page_template'; +import { StreamsAppRouterBreadcrumb } from '../components/streams_app_router_breadcrumb'; +import { RedirectTo } from '../components/redirect_to'; +import { StreamListView } from '../components/stream_list_view'; + +/** + * The array of route definitions to be used when the application + * creates the routes. + */ +const streamsAppRoutes = { + '/': { + element: ( + <StreamsAppRouterBreadcrumb + title={i18n.translate('xpack.streams.appBreadcrumbTitle', { + defaultMessage: 'Streams', + })} + path="/" + > + <StreamsAppPageTemplate> + <Outlet /> + </StreamsAppPageTemplate> + </StreamsAppRouterBreadcrumb> + ), + children: { + '/{key}': { + element: <Outlet />, + params: t.type({ + path: t.type({ + key: t.string, + }), + }), + children: { + '/{key}': { + element: <RedirectTo path="/{key}/{tab}" params={{ path: { tab: 'overview' } }} />, + }, + '/{key}/{tab}': { + element: <StreamDetailView />, + params: t.type({ + path: t.type({ + tab: t.string, + }), + }), + }, + }, + }, + '/': { + element: <StreamListView />, + }, + }, + }, +} satisfies RouteMap; + +export type StreamsAppRoutes = typeof streamsAppRoutes; + +export const streamsAppRouter = createRouter(streamsAppRoutes); + +export type StreamsAppRouter = typeof streamsAppRouter; diff --git a/x-pack/plugins/streams_app/public/services/types.ts b/x-pack/plugins/streams_app/public/services/types.ts new file mode 100644 index 0000000000000..7f75493d2525c --- /dev/null +++ b/x-pack/plugins/streams_app/public/services/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StreamsAppServices {} diff --git a/x-pack/plugins/streams_app/public/types.ts b/x-pack/plugins/streams_app/public/types.ts new file mode 100644 index 0000000000000..58d44784fe031 --- /dev/null +++ b/x-pack/plugins/streams_app/public/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, +} from '@kbn/data-views-plugin/public'; +import type { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/public'; +import type { StreamsPluginSetup, StreamsPluginStart } from '@kbn/streams-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { SharePublicSetup, SharePublicStart } from '@kbn/share-plugin/public/plugin'; +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface StreamsAppSetupDependencies { + streams: StreamsPluginSetup; + data: DataPublicPluginSetup; + dataViews: DataViewsPublicPluginSetup; + observabilityShared: ObservabilitySharedPluginSetup; + unifiedSearch: {}; + share: SharePublicSetup; +} + +export interface StreamsAppStartDependencies { + streams: StreamsPluginStart; + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; + observabilityShared: ObservabilitySharedPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; + share: SharePublicStart; +} + +export interface StreamsAppPublicSetup {} + +export interface StreamsAppPublicStart {} diff --git a/x-pack/plugins/streams_app/public/util/esql_result_to_timeseries.ts b/x-pack/plugins/streams_app/public/util/esql_result_to_timeseries.ts new file mode 100644 index 0000000000000..c32bbf89135bd --- /dev/null +++ b/x-pack/plugins/streams_app/public/util/esql_result_to_timeseries.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async'; +import type { UnparsedEsqlResponse } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; +import { orderBy } from 'lodash'; + +interface Timeseries<T extends string> { + id: string; + label: string; + metricNames: T[]; + data: Array<{ x: number } & Record<T, number | null>>; +} + +export function esqlResultToTimeseries<T extends string>({ + result, + metricNames, +}: { + result: AbortableAsyncState<UnparsedEsqlResponse>; + metricNames: T[]; +}): Array<Timeseries<T>> { + const columns = result.value?.columns; + + const rows = result.value?.values; + + if (!columns?.length || !rows?.length) { + return []; + } + + const timestampColumn = columns.find((col) => col.name === '@timestamp'); + + if (!timestampColumn) { + return []; + } + + const collectedSeries: Map<string, Timeseries<T>> = new Map(); + + rows.forEach((columnsInRow) => { + const values = new Map<string, number | null>(); + const labels = new Map<string, string>(); + let timestamp: number; + + columnsInRow.forEach((value, index) => { + const column = columns[index]; + const isTimestamp = column.name === '@timestamp'; + const isMetric = metricNames.indexOf(column.name as T) !== -1; + + if (isTimestamp) { + timestamp = new Date(value as string | number).getTime(); + } else if (isMetric) { + values.set(column.name, value as number | null); + } else { + labels.set(column.name, String(value)); + } + }); + + const seriesKey = + Array.from(labels.entries()) + .map(([key, value]) => [key, value].join(':')) + .sort() + .join(',') || '-'; + + if (!collectedSeries.has(seriesKey)) { + collectedSeries.set(seriesKey, { + id: seriesKey, + data: [], + label: seriesKey, + metricNames, + }); + } + + const series = collectedSeries.get(seriesKey)!; + + const coordinate = { + x: timestamp!, + } as { x: number } & Record<T, number | null>; + + values.forEach((value, key) => { + if (key !== 'x') { + // @ts-expect-error + coordinate[key as T] = value; + } + }); + + series.data.push(coordinate); + + return collectedSeries; + }); + + return Array.from(collectedSeries.entries()).map(([id, timeseries]) => { + return { + ...timeseries, + data: orderBy(timeseries.data, 'x', 'asc'), + }; + }); +} diff --git a/x-pack/plugins/streams_app/server/config.ts b/x-pack/plugins/streams_app/server/config.ts new file mode 100644 index 0000000000000..73e631c7f6d9f --- /dev/null +++ b/x-pack/plugins/streams_app/server/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; + +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type StreamsAppConfig = TypeOf<typeof config>; diff --git a/x-pack/plugins/streams_app/server/index.ts b/x-pack/plugins/streams_app/server/index.ts new file mode 100644 index 0000000000000..1ca3b3c6e3de7 --- /dev/null +++ b/x-pack/plugins/streams_app/server/index.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 type { + PluginConfigDescriptor, + PluginInitializer, + PluginInitializerContext, +} from '@kbn/core/server'; +import type { StreamsAppConfig } from './config'; +import { StreamsAppPlugin } from './plugin'; +import type { + StreamsAppServerSetup, + StreamsAppServerStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies, +} from './types'; + +export type { StreamsAppServerSetup, StreamsAppServerStart }; + +import { config as configSchema } from './config'; + +export const config: PluginConfigDescriptor<StreamsAppConfig> = { + schema: configSchema, +}; + +export const plugin: PluginInitializer< + StreamsAppServerSetup, + StreamsAppServerStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies +> = async (pluginInitializerContext: PluginInitializerContext<StreamsAppConfig>) => + new StreamsAppPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/streams_app/server/plugin.ts b/x-pack/plugins/streams_app/server/plugin.ts new file mode 100644 index 0000000000000..00fb61c1a9ccf --- /dev/null +++ b/x-pack/plugins/streams_app/server/plugin.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 type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import type { + ConfigSchema, + StreamsAppServerSetup, + StreamsAppServerStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies, +} from './types'; + +export class StreamsAppPlugin + implements + Plugin< + StreamsAppServerSetup, + StreamsAppServerStart, + StreamsAppSetupDependencies, + StreamsAppStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext<ConfigSchema>) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup<StreamsAppStartDependencies, StreamsAppServerStart>, + pluginsSetup: StreamsAppSetupDependencies + ): StreamsAppServerSetup { + return {}; + } + + start(core: CoreStart, pluginsStart: StreamsAppStartDependencies): StreamsAppServerStart { + return {}; + } +} diff --git a/x-pack/plugins/streams_app/server/types.ts b/x-pack/plugins/streams_app/server/types.ts new file mode 100644 index 0000000000000..e425ae7422d75 --- /dev/null +++ b/x-pack/plugins/streams_app/server/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { StreamsPluginStart, StreamsPluginSetup } from '@kbn/streams-plugin/server'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface StreamsAppSetupDependencies { + streams: StreamsPluginSetup; +} + +export interface StreamsAppStartDependencies { + streams: StreamsPluginStart; +} + +export interface StreamsAppServerSetup {} + +export interface StreamsAppServerStart {} diff --git a/x-pack/plugins/streams_app/tsconfig.json b/x-pack/plugins/streams_app/tsconfig.json new file mode 100644 index 0000000000000..39acb94665ae5 --- /dev/null +++ b/x-pack/plugins/streams_app/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*", + ".storybook/**/*" + ], + "exclude": ["target/**/*", ".storybook/**/*.js"], + "kbn_references": [ + "@kbn/core", + "@kbn/data-plugin", + "@kbn/data-views-plugin", + "@kbn/observability-shared-plugin", + "@kbn/unified-search-plugin", + "@kbn/react-kibana-context-render", + "@kbn/shared-ux-link-redirect-app", + "@kbn/typed-react-router-config", + "@kbn/i18n", + "@kbn/observability-utils-browser", + "@kbn/kibana-react-plugin", + "@kbn/es-query", + "@kbn/logging", + "@kbn/deeplinks-observability", + "@kbn/config-schema", + "@kbn/calculate-auto", + "@kbn/streams-plugin", + "@kbn/share-plugin", + "@kbn/observability-utils-server", + ] +} diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts index 23ef344c197fc..f5b3912eabd3b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts @@ -644,7 +644,7 @@ describe('estimateCapacity', () => { value: expect.any(Object), }); expect(logger.warn).toHaveBeenCalledWith( - 'Task Manager is unhealthy, the assumedAverageRecurringRequiredThroughputPerMinutePerKibana (175) < capacityPerMinutePerKibana (200)' + 'Task Manager is unhealthy, the assumedRequiredThroughputPerMinutePerKibana (215) >= capacityPerMinutePerKibana (200)' ); }); @@ -710,7 +710,7 @@ describe('estimateCapacity', () => { value: expect.any(Object), }); expect(logger.warn).toHaveBeenCalledWith( - 'Task Manager is unhealthy, the assumedRequiredThroughputPerMinutePerKibana (250) >= capacityPerMinutePerKibana (200) AND assumedAverageRecurringRequiredThroughputPerMinutePerKibana (210) >= capacityPerMinutePerKibana (200)' + 'Task Manager is unhealthy, the assumedAverageRecurringRequiredThroughputPerMinutePerKibana (210) > capacityPerMinutePerKibana (200)' ); }); diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts index acbf1284b21b7..a4a9c963e8ee3 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -248,13 +248,13 @@ function getHealthStatus( return { status: HealthStatus.OK, reason }; } - if (assumedAverageRecurringRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana) { - const reason = `Task Manager is unhealthy, the assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) < capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; + if (assumedAverageRecurringRequiredThroughputPerMinutePerKibana > capacityPerMinutePerKibana) { + const reason = `Task Manager is unhealthy, the assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) > capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; logger.warn(reason); return { status: HealthStatus.OK, reason }; } - const reason = `Task Manager is unhealthy, the assumedRequiredThroughputPerMinutePerKibana (${assumedRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana}) AND assumedAverageRecurringRequiredThroughputPerMinutePerKibana (${assumedAverageRecurringRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; + const reason = `Task Manager is unhealthy, the assumedRequiredThroughputPerMinutePerKibana (${assumedRequiredThroughputPerMinutePerKibana}) >= capacityPerMinutePerKibana (${capacityPerMinutePerKibana})`; logger.warn(reason); return { status: HealthStatus.OK, reason }; } diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 0a96a22fb2679..3e335a7edd278 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -67,3 +67,5 @@ export type { } from './search_strategy'; export { Direction, EntityType, EMPTY_BROWSER_FIELDS } from './search_strategy'; + +export { getDataFromFieldsHits, toArray, isGeoField, toObjectArrayOfStrings } from './utils'; diff --git a/x-pack/plugins/timelines/common/utils/field_formatters.test.ts b/x-pack/plugins/timelines/common/utils/field_formatters.test.ts index 43babd374d991..46eb77c8f7f32 100644 --- a/x-pack/plugins/timelines/common/utils/field_formatters.test.ts +++ b/x-pack/plugins/timelines/common/utils/field_formatters.test.ts @@ -7,7 +7,7 @@ import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid'; import { EventHit } from '../search_strategy'; -import { getDataFromFieldsHits, getDataSafety } from './field_formatters'; +import { getDataFromFieldsHits } from './field_formatters'; describe('Events Details Helpers', () => { const fields: EventHit['fields'] = eventHit.fields; @@ -84,7 +84,7 @@ describe('Events Details Helpers', () => { }, ]; const result = getDataFromFieldsHits(whackFields); - expect(result).toEqual(whackResultFields); + expect(result).toMatchObject(whackResultFields); }); it('flattens alert parameters', () => { const ruleParameterFields = { @@ -191,7 +191,7 @@ describe('Events Details Helpers', () => { ]; const result = getDataFromFieldsHits(ruleParameterFields); - expect(result).toEqual(ruleParametersResultFields); + expect(result).toMatchObject(ruleParametersResultFields); }); it('get data from threat enrichments', () => { @@ -546,6 +546,17 @@ describe('Events Details Helpers', () => { originalValue: ['495ad7a7-316e-4544-8a0f-9c098daee76e'], values: ['495ad7a7-316e-4544-8a0f-9c098daee76e'], }, + { + category: 'threat', + field: 'threat.enrichments', + isObjectArray: true, + originalValue: [ + '{"matched.field":["myhash.mysha256"],"matched.index":["logs-ti_abusech.malware"],"matched.type":["indicator_match_rule"],"feed.name":["AbuseCH malware"],"matched.atomic":["a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3"]}', + ], + values: [ + '{"matched.field":["myhash.mysha256"],"matched.index":["logs-ti_abusech.malware"],"matched.type":["indicator_match_rule"],"feed.name":["AbuseCH malware"],"matched.atomic":["a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3"]}', + ], + }, { category: 'threat', field: 'threat.enrichments.matched.field', @@ -581,25 +592,9 @@ describe('Events Details Helpers', () => { originalValue: ['a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3'], values: ['a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3'], }, - { - category: 'threat', - field: 'threat.enrichments', - isObjectArray: true, - originalValue: [ - '{"matched.field":["myhash.mysha256"],"matched.index":["logs-ti_abusech.malware"],"matched.type":["indicator_match_rule"],"feed.name":["AbuseCH malware"],"matched.atomic":["a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3"]}', - ], - values: [ - '{"matched.field":["myhash.mysha256"],"matched.index":["logs-ti_abusech.malware"],"matched.type":["indicator_match_rule"],"feed.name":["AbuseCH malware"],"matched.atomic":["a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3"]}', - ], - }, ]; const result = getDataFromFieldsHits(data); - expect(result).toEqual(ruleParametersResultFields); + expect(result).toMatchObject(ruleParametersResultFields); }); }); - - it('#getDataSafety', async () => { - const result = await getDataSafety(getDataFromFieldsHits, fields); - expect(result).toEqual(resultFields); - }); }); diff --git a/x-pack/plugins/timelines/common/utils/field_formatters.ts b/x-pack/plugins/timelines/common/utils/field_formatters.ts index c9292987f59b2..2e3785633bc3f 100644 --- a/x-pack/plugins/timelines/common/utils/field_formatters.ts +++ b/x-pack/plugins/timelines/common/utils/field_formatters.ts @@ -8,9 +8,15 @@ import { isEmpty } from 'lodash/fp'; import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; -import { ecsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; -import { technicalRuleFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/technical_rule_field_map'; -import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; +import { + ecsFieldMap, + EcsFieldMap, +} from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; +import { + technicalRuleFieldMap, + TechnicalRuleFieldMap, +} from '@kbn/rule-registry-plugin/common/assets/field_maps/technical_rule_field_map'; +import { legacyExperimentalFieldMap, ExperimentalRuleFieldMap } from '@kbn/alerts-as-data-utils'; import { Fields, TimelineEventsDetailsItem } from '../search_strategy'; import { toObjectArrayOfStrings, toStringArray } from './to_array'; import { ENRICHMENT_DESTINATION_PATH } from '../constants'; @@ -51,117 +57,141 @@ export const isRuleParametersFieldOrSubfield = (field: string, prependField?: st export const isThreatEnrichmentFieldOrSubfield = (field: string, prependField?: string) => prependField?.includes(ENRICHMENT_DESTINATION_PATH) || field === ENRICHMENT_DESTINATION_PATH; +// Helper functions +const createFieldItem = ( + fieldCategory: string, + field: string, + values: string[], + isObjectArray: boolean +): TimelineEventsDetailsItem => ({ + category: fieldCategory, + field, + values, + originalValue: values, + isObjectArray, +}); + +const processGeoField = ( + field: string, + item: unknown[], + fieldCategory: string +): TimelineEventsDetailsItem => { + const formattedLocation = formatGeoLocation(item); + return createFieldItem(fieldCategory, field, formattedLocation, true); +}; + +const processSimpleField = ( + dotField: string, + strArr: string[], + isObjectArray: boolean, + fieldCategory: string +): TimelineEventsDetailsItem => createFieldItem(fieldCategory, dotField, strArr, isObjectArray); + +const processNestedFields = ( + item: unknown, + dotField: string, + fieldCategory: string, + prependDotField: boolean +): TimelineEventsDetailsItem[] => { + if (Array.isArray(item)) { + return item.flatMap((curr) => + getDataFromFieldsHits(curr as Fields, prependDotField ? dotField : undefined, fieldCategory) + ); + } + + return getDataFromFieldsHits( + item as Fields, + prependDotField ? dotField : undefined, + fieldCategory + ); +}; + +type DisjointFieldNames = 'ecs.version' | 'event.action' | 'event.kind' | 'event.original'; + +// Memoized field maps +const fieldMaps: EcsFieldMap & + Omit<TechnicalRuleFieldMap, DisjointFieldNames> & + ExperimentalRuleFieldMap = { + ...technicalRuleFieldMap, + ...ecsFieldMap, + ...legacyExperimentalFieldMap, +}; + export const getDataFromFieldsHits = ( fields: Fields, prependField?: string, prependFieldCategory?: string -): TimelineEventsDetailsItem[] => - Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((accumulator, field) => { +): TimelineEventsDetailsItem[] => { + const resultMap = new Map<string, TimelineEventsDetailsItem>(); + const fieldNames = Object.keys(fields); + for (let i = 0; i < fieldNames.length; i++) { + const field = fieldNames[i]; const item: unknown[] = fields[field]; - const fieldCategory = - prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); + const fieldCategory = prependFieldCategory ?? getFieldCategory(field); + const dotField = prependField ? `${prependField}.${field}` : field; + + // Handle geo fields if (isGeoField(field)) { - return [ - ...accumulator, - { - category: fieldCategory, - field, - values: formatGeoLocation(item), - originalValue: formatGeoLocation(item), - isObjectArray: true, // important for UI - }, - ]; + const geoItem = processGeoField(field, item, fieldCategory); + resultMap.set(field, geoItem); + // eslint-disable-next-line no-continue + continue; } + const objArrStr = toObjectArrayOfStrings(item); const strArr = objArrStr.map(({ str }) => str); const isObjectArray = objArrStr.some((o) => o.isObjectArray); - const dotField = prependField ? `${prependField}.${field}` : field; - // return simple field value (non-ecs object, non-array) - if ( - !isObjectArray || - (Object.keys({ - ...ecsFieldMap, - ...technicalRuleFieldMap, - ...legacyExperimentalFieldMap, - }).find((ecsField) => ecsField === field) === undefined && - !isRuleParametersFieldOrSubfield(field, prependField)) - ) { - return [ - ...accumulator, - { - category: fieldCategory, - field: dotField, - values: strArr, - originalValue: strArr, - isObjectArray, - }, - ]; + const isEcsField = fieldMaps[field as keyof typeof fieldMaps] !== undefined; + const isRuleParameters = isRuleParametersFieldOrSubfield(field, prependField); + + // Handle simple fields + if (!isObjectArray || (!isEcsField && !isRuleParameters)) { + const simpleItem = processSimpleField(dotField, strArr, isObjectArray, fieldCategory); + resultMap.set(dotField, simpleItem); + // eslint-disable-next-line no-continue + continue; } - const threatEnrichmentObject = isThreatEnrichmentFieldOrSubfield(field, prependField) - ? [ - { - category: fieldCategory, - field: dotField, - values: strArr, - originalValue: strArr, - isObjectArray, - }, - ] - : []; - - // format nested fields - let nestedFields: TimelineEventsDetailsItem[] = []; - if (isRuleParametersFieldOrSubfield(field, prependField)) { - nestedFields = Array.isArray(item) - ? item - .reduce<TimelineEventsDetailsItem[][]>((acc, curr) => { - acc.push(getDataFromFieldsHits(curr as Fields, dotField, fieldCategory)); - return acc; - }, []) - .flat() - : getDataFromFieldsHits(item, dotField, fieldCategory); - } else { - nestedFields = Array.isArray(item) - ? item - .reduce<TimelineEventsDetailsItem[][]>((acc, curr) => { - acc.push(getDataFromFieldsHits(curr as Fields, dotField, fieldCategory)); - return acc; - }, []) - .flat() - : getDataFromFieldsHits(item, prependField, fieldCategory); + // Handle threat enrichment + if (isThreatEnrichmentFieldOrSubfield(field, prependField)) { + const enrichmentItem = createFieldItem(fieldCategory, dotField, strArr, isObjectArray); + resultMap.set(dotField, enrichmentItem); } - // combine duplicate fields - const flat: Record<string, TimelineEventsDetailsItem> = [ - ...accumulator, - ...nestedFields, - ...threatEnrichmentObject, - ].reduce( - (acc, f) => ({ - ...acc, - // acc/flat is hashmap to determine if we already have the field or not without an array iteration - // its converted back to array in return with Object.values - ...(acc[f.field] != null - ? { - [f.field]: { - ...f, - originalValue: acc[f.field].originalValue.includes(f.originalValue[0]) - ? acc[f.field].originalValue - : [...acc[f.field].originalValue, ...f.originalValue], - values: acc[f.field].values?.includes(f.values?.[0] || '') - ? acc[f.field].values - : [...(acc[f.field].values || []), ...(f.values || [])], - }, - } - : { [f.field]: f }), - }), - {} as Record<string, TimelineEventsDetailsItem> + // Process nested fields + const nestedFields = processNestedFields( + item, + dotField, + fieldCategory, + isRuleParameters || isThreatEnrichmentFieldOrSubfield(field, prependField) ); + // Merge results + for (const nestedItem of nestedFields) { + const existing = resultMap.get(nestedItem.field); + + if (!existing) { + resultMap.set(nestedItem.field, nestedItem); + // eslint-disable-next-line no-continue + continue; + } - return Object.values(flat); - }, []); + // Merge values and originalValue arrays + const mergedValues = existing.values?.includes(nestedItem.values?.[0] || '') + ? existing.values + : [...(existing.values || []), ...(nestedItem.values || [])]; -export const getDataSafety = <A, T>(fn: (args: A) => T, args: A): Promise<T> => - new Promise((resolve) => setTimeout(() => resolve(fn(args)))); + const mergedOriginal = existing.originalValue.includes(nestedItem.originalValue[0]) + ? existing.originalValue + : [...existing.originalValue, ...nestedItem.originalValue]; + + resultMap.set(nestedItem.field, { + ...nestedItem, + values: mergedValues, + originalValue: mergedOriginal, + }); + } + } + + return Array.from(resultMap.values()); +}; diff --git a/x-pack/plugins/timelines/common/utils/index.ts b/x-pack/plugins/timelines/common/utils/index.ts new file mode 100644 index 0000000000000..e4b7eefec454f --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getDataFromFieldsHits, isGeoField } from './field_formatters'; +export { toArray, toObjectArrayOfStrings } from './to_array'; diff --git a/x-pack/plugins/timelines/common/utils/to_array.ts b/x-pack/plugins/timelines/common/utils/to_array.ts index fbb2b8d48a250..d13eb0578008b 100644 --- a/x-pack/plugins/timelines/common/utils/to_array.ts +++ b/x-pack/plugins/timelines/common/utils/to_array.ts @@ -5,83 +5,55 @@ * 2.0. */ -export const toArray = <T = string>(value: T | T[] | null): T[] => - Array.isArray(value) ? value : value == null ? [] : [value]; -export const toStringArray = <T = string>(value: T | T[] | null): string[] => { - if (Array.isArray(value)) { - return value.reduce<string[]>((acc, v) => { - if (v != null) { - switch (typeof v) { - case 'number': - case 'boolean': - return [...acc, v.toString()]; - case 'object': - try { - return [...acc, JSON.stringify(v)]; - } catch { - return [...acc, 'Invalid Object']; - } - case 'string': - return [...acc, v]; - default: - return [...acc, `${v}`]; - } +export const toArray = <T>(value: T | T[] | null | undefined): T[] => + value == null ? [] : Array.isArray(value) ? value : [value]; + +export const toStringArray = <T>(value: T | T[] | null): string[] => { + if (value == null) return []; + + const arr = Array.isArray(value) ? value : [value]; + return arr.reduce<string[]>((acc, v) => { + if (v == null) return acc; + + if (typeof v === 'object') { + try { + acc.push(JSON.stringify(v)); + } catch { + acc.push('Invalid Object'); } return acc; - }, []); - } else if (value == null) { - return []; - } else if (!Array.isArray(value) && typeof value === 'object') { - try { - return [JSON.stringify(value)]; - } catch { - return ['Invalid Object']; } - } else { - return [`${value}`]; - } + + acc.push(String(v)); + return acc; + }, []); }; -export const toObjectArrayOfStrings = <T = string>( + +export const toObjectArrayOfStrings = <T>( value: T | T[] | null ): Array<{ str: string; isObjectArray?: boolean; }> => { - if (Array.isArray(value)) { - return value.reduce< - Array<{ - str: string; - isObjectArray?: boolean; - }> - >((acc, v) => { - if (v != null) { - switch (typeof v) { - case 'number': - case 'boolean': - return [...acc, { str: v.toString() }]; - case 'object': - try { - return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value - } catch { - return [...acc, { str: 'Invalid Object' }]; - } - case 'string': - return [...acc, { str: v }]; - default: - return [...acc, { str: `${v}` }]; - } + if (value == null) return []; + + const arr = Array.isArray(value) ? value : [value]; + return arr.reduce<Array<{ str: string; isObjectArray?: boolean }>>((acc, v) => { + if (v == null) return acc; + + if (typeof v === 'object') { + try { + acc.push({ + str: JSON.stringify(v), + isObjectArray: true, + }); + } catch { + acc.push({ str: 'Invalid Object' }); } return acc; - }, []); - } else if (value == null) { - return []; - } else if (!Array.isArray(value) && typeof value === 'object') { - try { - return [{ str: JSON.stringify(value), isObjectArray: true }]; - } catch { - return [{ str: 'Invalid Object' }]; } - } else { - return [{ str: `${value}` }]; - } + + acc.push({ str: String(v) }); + return acc; + }, []); }; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts index 2b4f562954df1..645f6daa5727d 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts @@ -17,7 +17,6 @@ import { } from '../../../../common/search_strategy'; import { TimelineEqlResponse } from '../../../../common/search_strategy/timeline/events/eql'; import { inspectStringifyObject } from '../../../utils/build_query'; -import { TIMELINE_EVENTS_FIELDS } from '../factory/helpers/constants'; import { formatTimelineData } from '../factory/helpers/format_timeline_data'; export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record<string, unknown> => { @@ -68,38 +67,38 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record<string, }, }; }; -const parseSequences = async (sequences: Array<EqlSequence<unknown>>, fieldRequested: string[]) => - sequences.reduce<Promise<TimelineEdges[]>>(async (acc, sequence, sequenceIndex) => { +const parseSequences = async (sequences: Array<EqlSequence<unknown>>, fieldRequested: string[]) => { + let result: TimelineEdges[] = []; + + for (const [sequenceIndex, sequence] of sequences.entries()) { const sequenceParentId = sequence.events[0]?._id ?? null; - const data = await acc; - const allData = await Promise.all( - sequence.events.map(async (event, eventIndex) => { - const item = await formatTimelineData( - fieldRequested, - TIMELINE_EVENTS_FIELDS, - event as EventHit - ); - return Promise.resolve({ - ...item, - node: { - ...item.node, - ecs: { - ...item.node.ecs, - ...(sequenceParentId != null - ? { - eql: { - parentId: sequenceParentId, - sequenceNumber: `${sequenceIndex}-${eventIndex}`, - }, - } - : {}), - }, - }, - }); - }) + const formattedEvents = await formatTimelineData( + sequence.events as EventHit[], + fieldRequested, + false ); - return Promise.resolve([...data, ...allData]); - }, Promise.resolve([])); + + const eventsWithEql = formattedEvents.map((item, eventIndex) => ({ + ...item, + node: { + ...item.node, + ecs: { + ...item.node.ecs, + ...(sequenceParentId && { + eql: { + parentId: sequenceParentId, + sequenceNumber: `${sequenceIndex}-${eventIndex}`, + }, + }), + }, + }, + })); + + result = result.concat(eventsWithEql); + } + + return result; +}; export const parseEqlResponse = async ( options: TimelineEqlRequestOptions, @@ -116,10 +115,10 @@ export const parseEqlResponse = async ( if (response.rawResponse.hits.sequences !== undefined) { edges = await parseSequences(response.rawResponse.hits.sequences, options.fieldRequested); } else if (response.rawResponse.hits.events !== undefined) { - edges = await Promise.all( - response.rawResponse.hits.events.map(async (event) => - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit) - ) + edges = await formatTimelineData( + response.rawResponse.hits.events as EventHit[], + options.fieldRequested, + false ); } diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index 4ed857b4885e3..2ee2b64162c13 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -14,13 +14,11 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants import { EventHit, TimelineEventsAllStrategyResponse, - TimelineEdges, } from '../../../../../../common/search_strategy'; import { TimelineFactory } from '../../types'; import { buildTimelineEventsAllQuery } from './query.events_all.dsl'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { formatTimelineData } from '../../helpers/format_timeline_data'; -import { TIMELINE_EVENTS_FIELDS } from '../../helpers/constants'; export const timelineEventsAll: TimelineFactory<TimelineEventsQueries.all> = { buildDsl: ({ authFilter, ...options }) => { @@ -54,14 +52,10 @@ export const timelineEventsAll: TimelineFactory<TimelineEventsQueries.all> = { fieldRequested = [...new Set(fieldsReturned)]; } - const edges: TimelineEdges[] = await Promise.all( - hits.map((hit) => - formatTimelineData( - fieldRequested, - options.excludeEcsData ? [] : TIMELINE_EVENTS_FIELDS, - hit as EventHit - ) - ) + const edges = await formatTimelineData( + hits as EventHit[], + fieldRequested, + options.excludeEcsData ?? false ); const consumers = producerBuckets.reduce( diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts index 46edcf926d061..fcb90b73c241e 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts @@ -12,15 +12,11 @@ import { TimelineEventsQueries } from '../../../../../../common/api/search_strat import { EventHit, TimelineEventsDetailsStrategyResponse, - TimelineEventsDetailsItem, } from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { TimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { - getDataFromFieldsHits, - getDataSafety, -} from '../../../../../../common/utils/field_formatters'; +import { getDataFromFieldsHits } from '../../../../../../common/utils/field_formatters'; import { buildEcsObjects } from '../../helpers/build_ecs_objects'; export const timelineEventsDetails: TimelineFactory<TimelineEventsQueries.details> = { @@ -57,10 +53,7 @@ export const timelineEventsDetails: TimelineFactory<TimelineEventsQueries.detail }; } - const fieldsData = await getDataSafety<EventHit['fields'], TimelineEventsDetailsItem[]>( - getDataFromFieldsHits, - merge(fields, hitsData) - ); + const fieldsData = getDataFromFieldsHits(merge(fields, hitsData)); const rawEventData = response.rawResponse.hits.hits[0]; const ecs = buildEcsObjects(rawEventData as EventHit); diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts index 5117f8dc889ed..3e96494c88313 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.test.ts @@ -7,12 +7,12 @@ import { eventHit } from '@kbn/securitysolution-t-grid'; import { EventHit } from '../../../../../common/search_strategy'; -import { TIMELINE_EVENTS_FIELDS } from './constants'; import { formatTimelineData } from './format_timeline_data'; describe('formatTimelineData', () => { it('should properly format the timeline data', async () => { const res = await formatTimelineData( + [eventHit], [ '@timestamp', 'host.name', @@ -21,187 +21,188 @@ describe('formatTimelineData', () => { 'source.geo.location', 'threat.enrichments.matched.field', ], - TIMELINE_EVENTS_FIELDS, - eventHit + false ); - expect(res).toEqual({ - cursor: { - tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', - value: '1605624488922', - }, - node: { - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _index: 'auditbeat-7.8.0-2020.11.05-000003', - data: [ - { - field: '@timestamp', - value: ['2020-11-17T14:48:08.922Z'], - }, - { - field: 'host.name', - value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - }, - { - field: 'threat.enrichments.matched.field', - value: [ - 'matched_field', - 'other_matched_field', - 'matched_field_2', - 'host.name', - 'host.hostname', - 'host.architecture', - ], - }, - { - field: 'source.geo.location', - value: [`{"lon":118.7778,"lat":32.0617}`], - }, - ], - ecs: { - '@timestamp': ['2020-11-17T14:48:08.922Z'], + expect(res).toEqual([ + { + cursor: { + tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', + value: '1605624488922', + }, + node: { _id: 'tkCt1nUBaEgqnrVSZ8R_', _index: 'auditbeat-7.8.0-2020.11.05-000003', - agent: { - type: ['auditbeat'], - }, - event: { - action: ['process_started'], - category: ['process'], - dataset: ['process'], - kind: ['event'], - module: ['system'], - type: ['start'], - }, - host: { - id: ['e59991e835905c65ed3e455b33e13bd6'], - ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - os: { - family: ['debian'], + data: [ + { + field: '@timestamp', + value: ['2020-11-17T14:48:08.922Z'], }, - }, - message: ['Process go (PID: 4313) by user jenkins STARTED'], - process: { - args: ['go', 'vet', './...'], - entity_id: ['Z59cIkAAIw8ZoK0H'], - executable: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - hash: { - sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - }, - name: ['go'], - pid: ['4313'], - ppid: ['3977'], - working_directory: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - }, - timestamp: '2020-11-17T14:48:08.922Z', - user: { - name: ['jenkins'], - }, - threat: { - enrichments: [ - { - feed: { name: [] }, - indicator: { - provider: ['yourself'], - reference: [], - }, - matched: { - atomic: ['matched_atomic'], - field: ['matched_field', 'other_matched_field'], - type: [], - }, - }, - { - feed: { name: [] }, - indicator: { - provider: ['other_you'], - reference: [], - }, - matched: { - atomic: ['matched_atomic_2'], - field: ['matched_field_2'], - type: [], - }, - }, - { - feed: { - name: [], - }, - indicator: { - provider: [], - reference: [], - }, - matched: { - atomic: ['MacBook-Pro-de-Gloria.local'], - field: ['host.name'], - type: ['indicator_match_rule'], - }, + { + field: 'host.name', + value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + field: 'threat.enrichments.matched.field', + value: [ + 'matched_field', + 'other_matched_field', + 'matched_field_2', + 'host.name', + 'host.hostname', + 'host.architecture', + ], + }, + { + field: 'source.geo.location', + value: [`{"lon":118.7778,"lat":32.0617}`], + }, + ], + ecs: { + '@timestamp': ['2020-11-17T14:48:08.922Z'], + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + agent: { + type: ['auditbeat'], + }, + event: { + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + module: ['system'], + type: ['start'], + }, + host: { + id: ['e59991e835905c65ed3e455b33e13bd6'], + ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + os: { + family: ['debian'], }, - { - feed: { - name: [], - }, - indicator: { - provider: [], - reference: [], - }, - matched: { - atomic: ['MacBook-Pro-de-Gloria.local'], - field: ['host.hostname'], - type: ['indicator_match_rule'], - }, + }, + message: ['Process go (PID: 4313) by user jenkins STARTED'], + process: { + args: ['go', 'vet', './...'], + entity_id: ['Z59cIkAAIw8ZoK0H'], + executable: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + hash: { + sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], }, - { - feed: { - name: [], - }, - indicator: { - provider: [], - reference: [], + name: ['go'], + pid: ['4313'], + ppid: ['3977'], + working_directory: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + timestamp: '2020-11-17T14:48:08.922Z', + user: { + name: ['jenkins'], + }, + threat: { + enrichments: [ + { + feed: { name: [] }, + indicator: { + provider: ['yourself'], + reference: [], + }, + matched: { + atomic: ['matched_atomic'], + field: ['matched_field', 'other_matched_field'], + type: [], + }, }, - matched: { - atomic: ['x86_64'], - field: ['host.architecture'], - type: ['indicator_match_rule'], + { + feed: { name: [] }, + indicator: { + provider: ['other_you'], + reference: [], + }, + matched: { + atomic: ['matched_atomic_2'], + field: ['matched_field_2'], + type: [], + }, }, - }, - { - feed: { - name: [], + { + feed: { + name: [], + }, + indicator: { + provider: [], + reference: [], + }, + matched: { + atomic: ['MacBook-Pro-de-Gloria.local'], + field: ['host.name'], + type: ['indicator_match_rule'], + }, }, - indicator: { - provider: [], - reference: [], + { + feed: { + name: [], + }, + indicator: { + provider: [], + reference: [], + }, + matched: { + atomic: ['MacBook-Pro-de-Gloria.local'], + field: ['host.hostname'], + type: ['indicator_match_rule'], + }, }, - matched: { - atomic: ['MacBook-Pro-de-Gloria.local'], - field: ['host.name'], - type: ['indicator_match_rule'], + { + feed: { + name: [], + }, + indicator: { + provider: [], + reference: [], + }, + matched: { + atomic: ['x86_64'], + field: ['host.architecture'], + type: ['indicator_match_rule'], + }, }, - }, - { - feed: { - name: [], + { + feed: { + name: [], + }, + indicator: { + provider: [], + reference: [], + }, + matched: { + atomic: ['MacBook-Pro-de-Gloria.local'], + field: ['host.name'], + type: ['indicator_match_rule'], + }, }, - indicator: { - provider: [], - reference: [], + { + feed: { + name: [], + }, + indicator: { + provider: [], + reference: [], + }, + matched: { + atomic: ['MacBook-Pro-de-Gloria.local'], + field: ['host.hostname'], + type: ['indicator_match_rule'], + }, }, - matched: { - atomic: ['MacBook-Pro-de-Gloria.local'], - field: ['host.hostname'], - type: ['indicator_match_rule'], - }, - }, - ], + ], + }, }, }, }, - }); + ]); }); it('should properly format the rule signal results', async () => { @@ -240,57 +241,61 @@ describe('formatTimelineData', () => { expect( await formatTimelineData( + [response], ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], - TIMELINE_EVENTS_FIELDS, - response + false ) - ).toEqual({ - cursor: { - tiebreaker: null, - value: '', - }, - node: { - _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', - _index: '.siem-signals-patrykkopycinski-default-000007', - data: [ - { - field: '@timestamp', - value: ['2021-01-09T13:41:40.517Z'], - }, - ], - ecs: { - '@timestamp': ['2021-01-09T13:41:40.517Z'], - timestamp: '2021-01-09T13:41:40.517Z', + ).toEqual([ + { + cursor: { + tiebreaker: null, + value: '', + }, + node: { _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', _index: '.siem-signals-patrykkopycinski-default-000007', - event: { - kind: ['signal'], - }, - kibana: { - alert: { - original_time: ['2021-01-09T13:39:32.595Z'], - workflow_status: ['open'], - threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'], - severity: ['low'], - risk_score: ['21'], - rule: { - building_block_type: [], - exceptions_list: [], - from: ['now-360s'], - uuid: ['696c24e0-526d-11eb-836c-e1620268b945'], - name: ['Threshold test'], - to: ['now'], - type: ['threshold'], - version: ['1'], - timeline_id: [], - timeline_title: [], - note: [], + data: [ + { + field: '@timestamp', + value: ['2021-01-09T13:41:40.517Z'], + }, + ], + ecs: { + '@timestamp': ['2021-01-09T13:41:40.517Z'], + timestamp: '2021-01-09T13:41:40.517Z', + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _index: '.siem-signals-patrykkopycinski-default-000007', + event: { + kind: ['signal'], + }, + kibana: { + alert: { + original_time: ['2021-01-09T13:39:32.595Z'], + workflow_status: ['open'], + threshold_result: [ + '{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}', + ], + severity: ['low'], + risk_score: ['21'], + rule: { + building_block_type: [], + exceptions_list: [], + from: ['now-360s'], + uuid: ['696c24e0-526d-11eb-836c-e1620268b945'], + name: ['Threshold test'], + to: ['now'], + type: ['threshold'], + version: ['1'], + timeline_id: [], + timeline_title: [], + note: [], + }, }, }, }, }, }, - }); + ]); }); it('should properly format the inventory rule signal results', async () => { @@ -347,6 +352,7 @@ describe('formatTimelineData', () => { expect( await formatTimelineData( + [response], [ 'kibana.alert.status', '@timestamp', @@ -376,168 +382,169 @@ describe('formatTimelineData', () => { 'event.kind', 'kibana.alert.rule.parameters', ], - TIMELINE_EVENTS_FIELDS, - response + false ) - ).toEqual({ - cursor: { - tiebreaker: null, - value: '', - }, - node: { - _id: '3fef4a4c-3d96-4e79-b4e5-158a0461d577', - _index: '.internal.alerts-observability.metrics.alerts-default-000001', - data: [ - { - field: 'kibana.alert.rule.consumer', - value: ['infrastructure'], - }, - { - field: '@timestamp', - value: ['2022-07-21T22:38:57.888Z'], - }, - { - field: 'kibana.alert.workflow_status', - value: ['open'], - }, - { - field: 'kibana.alert.reason', - value: [ - 'CPU usage is 37.8% in the last 1 day for gke-edge-oblt-pool-1-9a60016d-7dvq. Alert when > 10%.', - ], - }, - { - field: 'kibana.alert.rule.name', - value: ['test 1212'], - }, - { - field: 'kibana.alert.rule.uuid', - value: ['15d82f10-0926-11ed-bece-6b0c033d0075'], - }, - { - field: 'kibana.alert.rule.parameters.sourceId', - value: ['default'], - }, - { - field: 'kibana.alert.rule.parameters.nodeType', - value: ['host'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.comparator', - value: ['>'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.timeSize', - value: ['1'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.metric', - value: ['cpu'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.threshold', - value: ['10'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.aggregation', - value: ['avg'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.id', - value: ['alert-custom-metric'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.field', - value: [''], - }, - { - field: 'kibana.alert.rule.parameters.criteria.customMetric.type', - value: ['custom'], - }, - { - field: 'kibana.alert.rule.parameters.criteria.timeUnit', - value: ['d'], - }, - { - field: 'event.action', - value: ['active'], - }, - { - field: 'event.kind', - value: ['signal'], - }, - { - field: 'kibana.alert.status', - value: ['active'], - }, - { - field: 'kibana.alert.duration.us', - value: ['9502040000'], - }, - { - field: 'kibana.alert.rule.category', - value: ['Inventory'], - }, - { - field: 'kibana.alert.uuid', - value: ['3fef4a4c-3d96-4e79-b4e5-158a0461d577'], - }, - { - field: 'kibana.alert.start', - value: ['2022-07-21T20:00:35.848Z'], - }, - { - field: 'kibana.alert.rule.producer', - value: ['infrastructure'], - }, - { - field: 'kibana.alert.rule.rule_type_id', - value: ['metrics.alert.inventory.threshold'], - }, - { - field: 'kibana.alert.instance.id', - value: ['gke-edge-oblt-pool-1-9a60016d-7dvq'], - }, - { - field: 'kibana.alert.rule.execution.uuid', - value: ['37498c42-0190-4a83-adfa-c7e5f817f977'], - }, - { - field: 'kibana.space_ids', - value: ['default'], - }, - { - field: 'kibana.version', - value: ['8.4.0'], - }, - ], - ecs: { - '@timestamp': ['2022-07-21T22:38:57.888Z'], + ).toEqual([ + { + cursor: { + tiebreaker: null, + value: '', + }, + node: { _id: '3fef4a4c-3d96-4e79-b4e5-158a0461d577', _index: '.internal.alerts-observability.metrics.alerts-default-000001', - event: { - action: ['active'], - kind: ['signal'], - }, - kibana: { - alert: { - reason: [ + data: [ + { + field: 'kibana.alert.rule.consumer', + value: ['infrastructure'], + }, + { + field: '@timestamp', + value: ['2022-07-21T22:38:57.888Z'], + }, + { + field: 'kibana.alert.workflow_status', + value: ['open'], + }, + { + field: 'kibana.alert.reason', + value: [ 'CPU usage is 37.8% in the last 1 day for gke-edge-oblt-pool-1-9a60016d-7dvq. Alert when > 10%.', ], - rule: { - consumer: ['infrastructure'], - name: ['test 1212'], - uuid: ['15d82f10-0926-11ed-bece-6b0c033d0075'], - parameters: [ - '{"sourceId":"default","nodeType":"host","criteria":[{"comparator":">","timeSize":1,"metric":"cpu","threshold":[10],"customMetric":{"aggregation":"avg","id":"alert-custom-metric","field":"","type":"custom"},"timeUnit":"d"}]}', + }, + { + field: 'kibana.alert.rule.name', + value: ['test 1212'], + }, + { + field: 'kibana.alert.rule.uuid', + value: ['15d82f10-0926-11ed-bece-6b0c033d0075'], + }, + { + field: 'kibana.alert.rule.parameters.sourceId', + value: ['default'], + }, + { + field: 'kibana.alert.rule.parameters.nodeType', + value: ['host'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.comparator', + value: ['>'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.timeSize', + value: ['1'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.metric', + value: ['cpu'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.threshold', + value: ['10'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.aggregation', + value: ['avg'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.id', + value: ['alert-custom-metric'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.field', + value: [''], + }, + { + field: 'kibana.alert.rule.parameters.criteria.customMetric.type', + value: ['custom'], + }, + { + field: 'kibana.alert.rule.parameters.criteria.timeUnit', + value: ['d'], + }, + { + field: 'event.action', + value: ['active'], + }, + { + field: 'event.kind', + value: ['signal'], + }, + { + field: 'kibana.alert.status', + value: ['active'], + }, + { + field: 'kibana.alert.duration.us', + value: ['9502040000'], + }, + { + field: 'kibana.alert.rule.category', + value: ['Inventory'], + }, + { + field: 'kibana.alert.uuid', + value: ['3fef4a4c-3d96-4e79-b4e5-158a0461d577'], + }, + { + field: 'kibana.alert.start', + value: ['2022-07-21T20:00:35.848Z'], + }, + { + field: 'kibana.alert.rule.producer', + value: ['infrastructure'], + }, + { + field: 'kibana.alert.rule.rule_type_id', + value: ['metrics.alert.inventory.threshold'], + }, + { + field: 'kibana.alert.instance.id', + value: ['gke-edge-oblt-pool-1-9a60016d-7dvq'], + }, + { + field: 'kibana.alert.rule.execution.uuid', + value: ['37498c42-0190-4a83-adfa-c7e5f817f977'], + }, + { + field: 'kibana.space_ids', + value: ['default'], + }, + { + field: 'kibana.version', + value: ['8.4.0'], + }, + ], + ecs: { + '@timestamp': ['2022-07-21T22:38:57.888Z'], + _id: '3fef4a4c-3d96-4e79-b4e5-158a0461d577', + _index: '.internal.alerts-observability.metrics.alerts-default-000001', + event: { + action: ['active'], + kind: ['signal'], + }, + kibana: { + alert: { + reason: [ + 'CPU usage is 37.8% in the last 1 day for gke-edge-oblt-pool-1-9a60016d-7dvq. Alert when > 10%.', ], + rule: { + consumer: ['infrastructure'], + name: ['test 1212'], + uuid: ['15d82f10-0926-11ed-bece-6b0c033d0075'], + parameters: [ + '{"sourceId":"default","nodeType":"host","criteria":[{"comparator":">","timeSize":1,"metric":"cpu","threshold":[10],"customMetric":{"aggregation":"avg","id":"alert-custom-metric","field":"","type":"custom"},"timeUnit":"d"}]}', + ], + }, + workflow_status: ['open'], }, - workflow_status: ['open'], }, + timestamp: '2022-07-21T22:38:57.888Z', }, - timestamp: '2022-07-21T22:38:57.888Z', }, }, - }); + ]); }); }); diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts index f56cfd32391d4..481b74a802fec 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts @@ -5,118 +5,130 @@ * 2.0. */ -import { get, has, merge, uniq } from 'lodash/fp'; -import { EventHit, TimelineEdges, TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { get, has } from 'lodash/fp'; +import { + EventHit, + TimelineEdges, + TimelineNonEcsData, + EventSource, +} from '../../../../../common/search_strategy'; import { toStringArray } from '../../../../../common/utils/to_array'; -import { getDataFromFieldsHits, getDataSafety } from '../../../../../common/utils/field_formatters'; +import { getDataFromFieldsHits } from '../../../../../common/utils/field_formatters'; import { getTimestamp } from './get_timestamp'; import { getNestedParentPath } from './get_nested_parent_path'; import { buildObjectRecursive } from './build_object_recursive'; -import { ECS_METADATA_FIELDS } from './constants'; +import { ECS_METADATA_FIELDS, TIMELINE_EVENTS_FIELDS } from './constants'; -export const formatTimelineData = async ( - dataFields: readonly string[], - ecsFields: readonly string[], - hit: EventHit -) => - uniq([...ecsFields, ...dataFields]).reduce<Promise<TimelineEdges>>( - async (acc, fieldName) => { - const flattenedFields: TimelineEdges = await acc; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - flattenedFields.node._id = hit._id!; - flattenedFields.node._index = hit._index; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - flattenedFields.node.ecs._id = hit._id!; - flattenedFields.node.ecs.timestamp = getTimestamp(hit); - flattenedFields.node.ecs._index = hit._index; - if (hit.sort && hit.sort.length > 1) { - flattenedFields.cursor.value = hit.sort[0]; - flattenedFields.cursor.tiebreaker = hit.sort[1]; - } - const waitForIt = await mergeTimelineFieldsWithHit( - fieldName, - flattenedFields, - hit, - dataFields, - ecsFields - ); - return Promise.resolve(waitForIt); - }, - Promise.resolve({ - node: { ecs: { _id: '' }, data: [], _id: '', _index: '' }, - cursor: { - value: '', - tiebreaker: null, - }, - }) - ); - -const getValuesFromFields = async ( +const createBaseTimelineEdges = (): TimelineEdges => ({ + node: { + ecs: { _id: '' }, + data: [], + _id: '', + _index: '', + }, + cursor: { + value: '', + tiebreaker: null, + }, +}); + +function deepMerge(target: EventSource, source: EventSource) { + for (const key in source) { + if (source && source[key] instanceof Object && target && target[key] instanceof Object) { + deepMerge(target[key], source[key]); + } else { + target[key] = source[key]; + } + } + return target; +} + +const processMetadataField = (fieldName: string, hit: EventHit): TimelineNonEcsData[] => [ + { + field: fieldName, + value: toStringArray(get(fieldName, hit)), + }, +]; + +const processFieldData = ( fieldName: string, hit: EventHit, nestedParentFieldName?: string -): Promise<TimelineNonEcsData[]> => { - if (ECS_METADATA_FIELDS.includes(fieldName)) { - return [{ field: fieldName, value: toStringArray(get(fieldName, hit)) }]; - } +): TimelineNonEcsData[] => { + const fieldToEval = nestedParentFieldName + ? { [nestedParentFieldName]: hit.fields[nestedParentFieldName] } + : { [fieldName]: hit.fields[fieldName] }; - let fieldToEval; - - if (nestedParentFieldName == null) { - fieldToEval = { - [fieldName]: hit.fields[fieldName], - }; - } else { - fieldToEval = { - [nestedParentFieldName]: hit.fields[nestedParentFieldName], - }; - } - const formattedData = await getDataSafety(getDataFromFieldsHits, fieldToEval); - return formattedData.reduce((acc: TimelineNonEcsData[], { field, values }) => { - // nested fields return all field values, pick only the one we asked for + const formattedData = getDataFromFieldsHits(fieldToEval); + const fieldsData: TimelineNonEcsData[] = []; + return formattedData.reduce((agg, { field, values }) => { if (field.includes(fieldName)) { - acc.push({ field, value: values }); + agg.push({ + field, + value: values, + }); } - return acc; - }, []); + return agg; + }, fieldsData); }; -const mergeTimelineFieldsWithHit = async <T>( - fieldName: string, - flattenedFields: T, - hit: EventHit, - dataFields: readonly string[], - ecsFields: readonly string[] -) => { - if (fieldName != null) { - const nestedParentPath = getNestedParentPath(fieldName, hit.fields); - if ( - nestedParentPath != null || - has(fieldName, hit.fields) || - ECS_METADATA_FIELDS.includes(fieldName) - ) { - const objectWithProperty = { - node: { - ...get('node', flattenedFields), - data: dataFields.includes(fieldName) - ? [ - ...get('node.data', flattenedFields), - ...(await getValuesFromFields(fieldName, hit, nestedParentPath)), - ] - : get('node.data', flattenedFields), - ecs: ecsFields.includes(fieldName) - ? { - ...get('node.ecs', flattenedFields), - ...buildObjectRecursive(fieldName, hit.fields), - } - : get('node.ecs', flattenedFields), - }, - }; - return merge(flattenedFields, objectWithProperty); +export const formatTimelineData = async ( + hits: EventHit[], + fieldRequested: readonly string[], + excludeEcsData: boolean +): Promise<TimelineEdges[]> => { + const ecsFields = excludeEcsData ? [] : TIMELINE_EVENTS_FIELDS; + + const uniqueFields = new Set([...ecsFields, ...fieldRequested]); + const dataFieldSet = new Set(fieldRequested); + const ecsFieldSet = new Set(ecsFields); + + const results: TimelineEdges[] = new Array(hits.length); + + for (let i = 0; i < hits.length; i++) { + const hit = hits[i]; + if (hit._id) { + const result = createBaseTimelineEdges(); + + result.node._id = hit._id; + result.node._index = hit._index; + result.node.ecs._id = hit._id; + result.node.ecs.timestamp = getTimestamp(hit); + result.node.ecs._index = hit._index; + + if (hit.sort?.length > 1) { + result.cursor.value = hit.sort[0]; + result.cursor.tiebreaker = hit.sort[1]; + } + + result.node.data = []; + + for (const fieldName of uniqueFields) { + const nestedParentPath = getNestedParentPath(fieldName, hit.fields); + const isEcs = ECS_METADATA_FIELDS.includes(fieldName); + if (!nestedParentPath && !has(fieldName, hit.fields) && !isEcs) { + // eslint-disable-next-line no-continue + continue; + } + + if (dataFieldSet.has(fieldName)) { + const values = isEcs + ? processMetadataField(fieldName, hit) + : processFieldData(fieldName, hit, nestedParentPath); + + result.node.data.push(...values); + } + + if (ecsFieldSet.has(fieldName)) { + deepMerge(result.node.ecs, buildObjectRecursive(fieldName, hit.fields)); + } + } + + results[i] = result; } else { - return flattenedFields; + results[i] = createBaseTimelineEdges(); } - } else { - return flattenedFields; } + + return results; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8bc9f6357c62b..9866bea029a98 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11527,7 +11527,6 @@ "xpack.apm.profiling.callout.dismiss": "Rejeter", "xpack.apm.profiling.callout.learnMore": "En savoir plus", "xpack.apm.profiling.callout.title": "Affichage des informations de profilage de l'hôte ou des hôtes exécutant des services {serviceName}", - "xpack.apm.profiling.flamegraph.filteredLabel": "Affichage des informations de profilage du ou des hôtes des services", "xpack.apm.profiling.flamegraph.link": "Accéder au flame-graph d'Universal Profiling", "xpack.apm.profiling.flamegraph.noDataFound": "Aucune donnée trouvée", "xpack.apm.profiling.tabs.flamegraph": "Flame-graph", @@ -15105,15 +15104,9 @@ "xpack.csp.grouping.nullGroupTooltip.groupingTitle": "regrouper par", "xpack.csp.integrationDashboard.noFindings.promptTitle": "Statut d'évaluation des résultats", "xpack.csp.integrationDashboard.noFindingsPrompt.promptDescription": "En attente de la collecte et de l'indexation des données. Si ce processus prend plus de temps que prévu, veuillez contacter notre support technique", - "xpack.csp.kspmIntegration.aksOption.benchmarkTitle": "CIS AKS", - "xpack.csp.kspmIntegration.aksOption.nameTitle": "AKS", - "xpack.csp.kspmIntegration.aksOption.tooltipContent": "Azure Kubernetes Service - Bientôt disponible", "xpack.csp.kspmIntegration.eksOption.benchmarkTitle": "CIS EKS", "xpack.csp.kspmIntegration.eksOption.nameTitle": "EKS", "xpack.csp.kspmIntegration.eksOption.tooltipContent": "EKS (Elastic Kubernetes Service)", - "xpack.csp.kspmIntegration.gkeOption.benchmarkTitle": "CIS GKE", - "xpack.csp.kspmIntegration.gkeOption.nameTitle": "GKE", - "xpack.csp.kspmIntegration.gkeOption.tooltipContent": "Google Kubernetes Engine - Bientôt disponible", "xpack.csp.kspmIntegration.integration.nameTitle": "Gestion du niveau de sécurité Kubernetes", "xpack.csp.kspmIntegration.integration.shortNameTitle": "KSPM", "xpack.csp.kspmIntegration.vanillaOption.benchmarkTitle": "CIS Kubernetes", @@ -15431,7 +15424,6 @@ "xpack.datasetQuality.types.label": "Types", "xpack.dataUsage.charts.ingestedMax": "Données ingérées", "xpack.dataUsage.charts.retainedMax": "Données conservées dans le stockage", - "xpack.dataUsage.metrics.filter.clearAll": "Tout effacer", "xpack.dataUsage.metrics.filter.dataStreams": "Flux de données", "xpack.dataUsage.metrics.filter.emptyMessage": "Aucun {filterName} disponible", "xpack.dataUsage.metrics.filter.metricTypes": "Types d'indicateurs", @@ -17481,10 +17473,6 @@ "xpack.enterpriseSearch.content.connectors.deleteModal.delete.crawler.description": "Vous êtes sur le point de supprimer le robot d'indexation suivant :", "xpack.enterpriseSearch.content.connectors.deleteModal.syncsWarning.indexNameDescription": "Cette action ne peut pas être annulée. Veuillez saisir {connectorName} pour confirmer.", "xpack.enterpriseSearch.content.connectors.deleteModal.title": "Supprimer {connectorCount} connecteur ?", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.ariaLabel": "Modifier la description du connecteur", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.placeholder": "Ajouter une description", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.ariaLabel": "Modifier le nom du connecteur", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.placeholder": "Ajouter un nom pour votre connecteur", "xpack.enterpriseSearch.content.connectors.overview.connectorErrorCallOut.title": "Votre connecteur a rapporté une erreur", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.description": "Le connecteur va créer l'index lors de sa prochaine synchronisation. Vous pouvez également créer l’index {indexName} manuellement avec les paramètres et les mappings de votre choix.", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.title": "L'index attaché n'existe pas", @@ -18310,10 +18298,7 @@ "xpack.enterpriseSearch.createConnector..title": "Créer un connecteur", "xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged": "Géré par Elastic", "xpack.enterpriseSearch.createConnector.badgeType.selfManaged": "Autogéré", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel": "Bêta", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel": "Autogéré", "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text": "Choisir une source de données", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel": "Préversion technique", "xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel": "Configuration", "xpack.enterpriseSearch.createConnector.configurationStep.h4.finishUpLabel": "Terminer", "xpack.enterpriseSearch.createConnector.configurationStep.p.description": "Vous pouvez synchroniser manuellement vos données, planifier une synchronisation récurrente ou gérer vos domaines.", @@ -20495,7 +20480,6 @@ "xpack.fleet.agentPolicyForm.newAgentPolicyFieldLabel": "Nouveau nom de la stratégie d'agent", "xpack.fleet.agentPolicyForm.outputOptionDisabledTypeNotSupportedText": "La sortie {outputType} pour l'intégration des agents n'est pas prise en charge pour Fleet Server, Synthetics ou APM.", "xpack.fleet.agentPolicyForm.outputOptionDisableOutputTypeText": "La sortie {outputType} pour l'intégration des agents n'est pas prise en charge pour Fleet Server, Synthetics ou APM.", - "xpack.fleet.agentPolicyForm.spaceDescription": "Sélectionnez un ou plusieurs espaces pour cette politique ou créez un nouvel espace. {link}", "xpack.fleet.agentPolicyForm.spaceFieldLabel": "Espaces", "xpack.fleet.agentPolicyForm.systemMonitoringText": "Collecte des logs et des mesures du système", "xpack.fleet.agentPolicyForm.systemMonitoringTooltipText": "Cela ajoutera également une intégration {system} pour collecter les logs et les indicateurs du système.", @@ -23322,7 +23306,6 @@ "xpack.idxMgmt.mappingsEditor.parameters.validations.fieldDataFrequency.numberGreaterThanOneErrorMessage": "La valeur doit être supérieure à un.", "xpack.idxMgmt.mappingsEditor.parameters.validations.greaterThanZeroErrorMessage": "Le facteur de montée en charge doit être supérieur à 0.", "xpack.idxMgmt.mappingsEditor.parameters.validations.ignoreAboveIsRequiredErrorMessage": "Limite de longueur de caractère obligatoire.", - "xpack.idxMgmt.mappingsEditor.parameters.validations.inferenceIdIsRequiredErrorMessage": "L’ID d’inférence est requis.", "xpack.idxMgmt.mappingsEditor.parameters.validations.localeFieldRequiredErrorMessage": "Spécifiez un paramètre régional.", "xpack.idxMgmt.mappingsEditor.parameters.validations.maxInputLengthFieldRequiredErrorMessage": "Spécifiez une longueur d'entrée maximale.", "xpack.idxMgmt.mappingsEditor.parameters.validations.nameIsRequiredErrorMessage": "Donnez un nom au champ.", @@ -26366,7 +26349,6 @@ "xpack.lens.app.createVisualizationLabel": "ES|QL", "xpack.lens.app.docLoadingError": "Erreur lors du chargement du document enregistré", "xpack.lens.app.editLensEmbeddableLabel": "Modifier la visualisation", - "xpack.lens.app.editVisualizationLabel": "Modifier la visualisation {lang}", "xpack.lens.app.exploreDataInDiscover": "Explorer dans Discover", "xpack.lens.app.exploreDataInDiscoverDrilldown": "Ouvrir dans Discover", "xpack.lens.app.exploreDataInDiscoverDrilldown.newTabConfig": "Ouvrir dans un nouvel onglet", @@ -26524,14 +26506,9 @@ "xpack.lens.editorFrame.suggestionPanelTitle": "Suggestions", "xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel": "Veuillez retirer {dimensionsTooMany, plural, one {une dimension} other {{dimensionsTooMany} dimensions}}", "xpack.lens.editorFrame.workspaceLabel": "Espace de travail", - "xpack.lens.embeddable.failure": "Impossible d'afficher la visualisation", - "xpack.lens.embeddable.featureBadge.iconDescription": "{count} {count, plural, one {modificateur} other {modificateurs}} de visualisation", - "xpack.lens.embeddable.fixErrors": "Effectuez des modifications dans l'éditeur Lens pour corriger l'erreur", - "xpack.lens.embeddable.legacyURLConflict.shortMessage": "Vous avez rencontré un conflit d’URL.", - "xpack.lens.embeddable.missingTimeRangeParam.longMessage": "La propriété timeRange est requise pour cette configuration.", - "xpack.lens.embeddable.missingTimeRangeParam.shortMessage": "Propriété timeRange manquante", - "xpack.lens.embeddable.moreErrors": "Effectuez des modifications dans l'éditeur Lens pour afficher plus d'erreurs", - "xpack.lens.embeddableDisplayName": "Lens", + "xpack.lens.featureBadge.iconDescription": "{count} {count, plural, one {modificateur} other {modificateurs}} de visualisation", + "xpack.lens.fixErrors": "Effectuez des modifications dans l'éditeur Lens pour corriger l'erreur", + "xpack.lens.moreErrors": "Effectuez des modifications dans l'éditeur Lens pour afficher plus d'erreurs", "xpack.lens.endValue.nearest": "La plus proche", "xpack.lens.endValue.none": "Masquer", "xpack.lens.endValue.zero": "Zéro", @@ -27041,7 +27018,6 @@ "xpack.lens.legacyMetric.titlePositions.bottom": "Bas", "xpack.lens.legacyMetric.titlePositions.top": "Haut", "xpack.lens.legacyUrlConflict.objectNoun": "Visualisation Lens", - "xpack.lens.lensSavedObjectLabel": "Visualisation Lens", "xpack.lens.lineCurve.smooth": "Lisser", "xpack.lens.lineCurve.step": "Étape", "xpack.lens.lineCurve.straight": "Droit", @@ -35462,10 +35438,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "Aucun cluster distant ne porte ce nom.", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "Impossible de modifier le cluster, aucune réponse renvoyée d'ES.", "xpack.reporting.breadcrumb": "Reporting", - "xpack.reporting.deprecations.csvPanelActionDownload.manualStep1": "Supprimez \"{enablePanelActionDownload}\" de `kibana.yml` ou modifiez le paramètre sur `false`.", - "xpack.reporting.deprecations.csvPanelActionDownload.manualStep2": "Utilisez le panneau de remplacement pour générer des rapports CSV à partir des panneaux de recherche enregistrés dans l'application Tableau de bord.", - "xpack.reporting.deprecations.csvPanelActionDownload.message": "Le paramètre \"{enablePanelActionDownload}\" est déclassé.", - "xpack.reporting.deprecations.csvPanelActionDownload.title": "Le paramètre permettant d'activer le téléchargement CSV à partir des panneaux de recherche enregistrés dans les tableaux de bord est déclassé.", "xpack.reporting.deprecations.migrateIndexIlmPolicy.manualStepOneMessage": "Mettez à jour tous les index de reporting de façon à ce qu'ils utilisent la politique \"{reportingIlmPolicy}\" à l'aide de l'API de paramètres des index.", "xpack.reporting.deprecations.migrateIndexIlmPolicyActionMessage": "Les nouveaux index de reporting seront gérés par la politique ILM provisionnée \"{reportingIlmPolicy}\". Vous devez modifier cette politique pour gérer le cycle de vie des rapports. Cette modification cible le modèle d'indexation du système caché \"{indexPattern}\".", "xpack.reporting.deprecations.migrateIndexIlmPolicyActionTitle": "Des index de reporting gérés par une politique ILM personnalisée ont été détectés.", @@ -36028,15 +36000,6 @@ "xpack.searchInferenceEndpoints.actions.endpointAddedSuccess": "Point de terminaison ajouté", "xpack.searchInferenceEndpoints.actions.endpointAddedSuccessDescription": "Le point de terminaison d'inférence \"{endpointId}\" a été ajouté.", "xpack.searchInferenceEndpoints.actions.trainedModelsStatGatherFailed": "Échec de la récupération des statistiques du modèle entraîné", - "xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription": "Les points de terminaison d'inférence vous permettent d'effectuer des tâches d'inférence à l'aide de modèles NLP fournis par des services tiers ou de modèles intégrés d'Elastic comme ELSER et E5. Configurez des tâches telles que l'incorporation de texte, les complétions, le reclassement et bien plus encore à l'aide de la fonction Créer une API d'inférence.", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description": "E5 est un modèle NLP tiers qui vous permet de réaliser des recherches sémantiques multilingues en utilisant des représentations vectorielles denses.", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title": "E5 multilingue", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription": "ELSER est le modèle NLP vectoriel creux d'Elastic pour la recherche sémantique en anglais.", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserTitle": "ELSER", - "xpack.searchInferenceEndpoints.addEmptyPrompt.learnHowToCreateInferenceEndpoints": "Découvrez comment créer des points de terminaison d'inférence", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithE5": "Recherche sémantique avec E5 multilingue", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithElser": "Recherche sémantique avec ELSER", - "xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel": "En savoir plus sur les modèles NLP intégrés :", "xpack.searchInferenceEndpoints.allInferenceEndpoints.description": "Les points de terminaison d'inférence rationalisent le déploiement et la gestion des modèles d'apprentissage automatique dans Elasticsearch. Configurez et gérez des tâches NLP à l'aide de points de terminaison uniques afin de créer une recherche basée sur l'IA.", "xpack.searchInferenceEndpoints.apiDocumentationLink": "Documentation sur les API", "xpack.searchInferenceEndpoints.cancel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b218ae98f07bb..08befe63cfa0a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11510,7 +11510,6 @@ "xpack.apm.profiling.callout.dismiss": "閉じる", "xpack.apm.profiling.callout.learnMore": "詳細", "xpack.apm.profiling.callout.title": "{serviceName}サービスを実行しているホストからプロファイリングインサイトを表示しています", - "xpack.apm.profiling.flamegraph.filteredLabel": "サービスのホストからプロファイリングインサイトを表示しています", "xpack.apm.profiling.flamegraph.link": "ユニバーサルプロファイリングFlamegraphに移動", "xpack.apm.profiling.flamegraph.noDataFound": "データが見つかりません", "xpack.apm.profiling.tabs.flamegraph": "Flamegraph", @@ -15084,15 +15083,9 @@ "xpack.csp.grouping.nullGroupTooltip.groupingTitle": "グループ分けの条件", "xpack.csp.integrationDashboard.noFindings.promptTitle": "調査結果評価ステータス", "xpack.csp.integrationDashboard.noFindingsPrompt.promptDescription": "データの収集とインデックス作成を待機しています。このプロセスに想定よりも時間がかかる場合は、サポートまでお問い合わせください", - "xpack.csp.kspmIntegration.aksOption.benchmarkTitle": "CIS AKS", - "xpack.csp.kspmIntegration.aksOption.nameTitle": "AKS", - "xpack.csp.kspmIntegration.aksOption.tooltipContent": "Azure Kubernetes Service - まもなくリリース", "xpack.csp.kspmIntegration.eksOption.benchmarkTitle": "CIS EKS", "xpack.csp.kspmIntegration.eksOption.nameTitle": "EKS", "xpack.csp.kspmIntegration.eksOption.tooltipContent": "Elastic Kubernetes Service", - "xpack.csp.kspmIntegration.gkeOption.benchmarkTitle": "CIS GKE", - "xpack.csp.kspmIntegration.gkeOption.nameTitle": "GKE", - "xpack.csp.kspmIntegration.gkeOption.tooltipContent": "Google Kubernetes Engine - まもなくリリース", "xpack.csp.kspmIntegration.integration.nameTitle": "Kubernetesセキュリティ態勢管理", "xpack.csp.kspmIntegration.integration.shortNameTitle": "KSPM", "xpack.csp.kspmIntegration.vanillaOption.benchmarkTitle": "CIS Kubernetes", @@ -15410,7 +15403,6 @@ "xpack.datasetQuality.types.label": "タイプ", "xpack.dataUsage.charts.ingestedMax": "インジェストされたデータ", "xpack.dataUsage.charts.retainedMax": "ストレージに保持されたデータ", - "xpack.dataUsage.metrics.filter.clearAll": "すべて消去", "xpack.dataUsage.metrics.filter.dataStreams": "データストリーム", "xpack.dataUsage.metrics.filter.emptyMessage": "{filterName}がありません", "xpack.dataUsage.metrics.filter.metricTypes": "メトリックタイプ", @@ -17456,10 +17448,6 @@ "xpack.enterpriseSearch.content.connectors.deleteModal.delete.crawler.description": "このコネクターを削除しようとしています:", "xpack.enterpriseSearch.content.connectors.deleteModal.syncsWarning.indexNameDescription": "この操作は元に戻すことができません。{connectorName}を入力して確認してください。", "xpack.enterpriseSearch.content.connectors.deleteModal.title": "\"{connectorCount}\"コネクターを削除しますか?", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.ariaLabel": "コネクターの説明を編集", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.placeholder": "説明を追加", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.ariaLabel": "コネクター名を編集", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.placeholder": "コネクターに名前を追加", "xpack.enterpriseSearch.content.connectors.overview.connectorErrorCallOut.title": "コネクターでエラーが発生しました", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.description": "コネクターは、次回の同期でインデックスを作成します。あるいは、任意の設定とマッピングを使用して、手動でインデックス{indexName}を作成できます。", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.title": "付けられたインデックスが存在しません", @@ -18284,10 +18272,7 @@ "xpack.enterpriseSearch.createConnector..title": "コネクターを作成する", "xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged": "Elasticマネージド", "xpack.enterpriseSearch.createConnector.badgeType.selfManaged": "自己管理", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel": "ベータ", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel": "自己管理", "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text": "データソースを選択", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel": "テクニカルプレビュー", "xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel": "構成", "xpack.enterpriseSearch.createConnector.configurationStep.h4.finishUpLabel": "終了", "xpack.enterpriseSearch.createConnector.configurationStep.p.description": "データを手動で同期したり、繰り返し同期をスケジュールしたり、ドメインを管理したりできます。", @@ -20464,7 +20449,6 @@ "xpack.fleet.agentPolicyForm.newAgentPolicyFieldLabel": "新しいエージェントポリシー名", "xpack.fleet.agentPolicyForm.outputOptionDisabledTypeNotSupportedText": "Fleet Server、Synthetics、APMではエージェント統合の{outputType}出力はサポートされていません。", "xpack.fleet.agentPolicyForm.outputOptionDisableOutputTypeText": "Fleet Server、Synthetics、APMではエージェント統合の{outputType}出力はサポートされていません。", - "xpack.fleet.agentPolicyForm.spaceDescription": "このポリシーに1つ以上のスペースを選択するか、新しいスペースを作成します。{link}", "xpack.fleet.agentPolicyForm.spaceFieldLabel": "スペース", "xpack.fleet.agentPolicyForm.systemMonitoringText": "システムログとメトリックの収集", "xpack.fleet.agentPolicyForm.systemMonitoringTooltipText": "これにより、{system}統合も追加され、システムログとメトリックを収集します。", @@ -23294,7 +23278,6 @@ "xpack.idxMgmt.mappingsEditor.parameters.validations.fieldDataFrequency.numberGreaterThanOneErrorMessage": "値は1よりも大きい値でなければなりません。", "xpack.idxMgmt.mappingsEditor.parameters.validations.greaterThanZeroErrorMessage": "スケーリングファクターは0よりも大きくなくてはなりません。", "xpack.idxMgmt.mappingsEditor.parameters.validations.ignoreAboveIsRequiredErrorMessage": "文字数制限が必要です。", - "xpack.idxMgmt.mappingsEditor.parameters.validations.inferenceIdIsRequiredErrorMessage": "推論IDは必須です。", "xpack.idxMgmt.mappingsEditor.parameters.validations.localeFieldRequiredErrorMessage": "ロケールを指定します。", "xpack.idxMgmt.mappingsEditor.parameters.validations.maxInputLengthFieldRequiredErrorMessage": "最大入力長さを指定します。", "xpack.idxMgmt.mappingsEditor.parameters.validations.nameIsRequiredErrorMessage": "フィールドに名前を付けます。", @@ -26337,7 +26320,6 @@ "xpack.lens.app.createVisualizationLabel": "ES|QL", "xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生", "xpack.lens.app.editLensEmbeddableLabel": "ビジュアライゼーションを編集", - "xpack.lens.app.editVisualizationLabel": "{lang}ビジュアライゼーションを編集", "xpack.lens.app.exploreDataInDiscover": "Discoverで探索", "xpack.lens.app.exploreDataInDiscoverDrilldown": "Discoverで開く", "xpack.lens.app.exploreDataInDiscoverDrilldown.newTabConfig": "新しいタブで開く", @@ -26496,14 +26478,13 @@ "xpack.lens.editorFrame.suggestionPanelTitle": "提案", "xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel": "{dimensionsTooMany, plural, other {{dimensionsTooMany} ディメンション}}を削除してください", "xpack.lens.editorFrame.workspaceLabel": "ワークスペース", - "xpack.lens.embeddable.failure": "ビジュアライゼーションを表示できませんでした", - "xpack.lens.embeddable.featureBadge.iconDescription": "{count}個のビジュアライゼーション{count, plural, other {修飾子}}", - "xpack.lens.embeddable.fixErrors": "Lensエディターで編集し、エラーを修正", - "xpack.lens.embeddable.legacyURLConflict.shortMessage": "URLの競合が発生しました", - "xpack.lens.embeddable.missingTimeRangeParam.longMessage": "指定された構成にはtimeRangeプロパティが必須です", - "xpack.lens.embeddable.missingTimeRangeParam.shortMessage": "timeRangeプロパティがありません", - "xpack.lens.embeddable.moreErrors": "Lensエディターで編集すると、エラーの詳細が表示されます", - "xpack.lens.embeddableDisplayName": "Lens", + "xpack.lens.failure": "ビジュアライゼーションを表示できませんでした", + "xpack.lens.featureBadge.iconDescription": "{count}個のビジュアライゼーション{count, plural, other {修飾子}}", + "xpack.lens.fixErrors": "Lensエディターで編集し、エラーを修正", + "xpack.lens.legacyURLConflict.shortMessage": "URLの競合が発生しました", + "xpack.lens.missingTimeRangeParam.longMessage": "指定された構成にはtimeRangeプロパティが必須です", + "xpack.lens.missingTimeRangeParam.shortMessage": "timeRangeプロパティがありません", + "xpack.lens.moreErrors": "Lensエディターで編集すると、エラーの詳細が表示されます", "xpack.lens.endValue.nearest": "最も近い", "xpack.lens.endValue.none": "非表示", "xpack.lens.endValue.zero": "ゼロ", @@ -27012,7 +26993,6 @@ "xpack.lens.legacyMetric.titlePositions.bottom": "一番下", "xpack.lens.legacyMetric.titlePositions.top": "トップ", "xpack.lens.legacyUrlConflict.objectNoun": "Lensビジュアライゼーション", - "xpack.lens.lensSavedObjectLabel": "Lensビジュアライゼーション", "xpack.lens.lineCurve.smooth": "平滑化", "xpack.lens.lineCurve.step": "手順", "xpack.lens.lineCurve.straight": "直線", @@ -35431,10 +35411,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.deprecations.csvPanelActionDownload.manualStep1": "kibana.ymlから“{enablePanelActionDownload}”を削除するか、設定をfalseに変更します。", - "xpack.reporting.deprecations.csvPanelActionDownload.manualStep2": "置換パネルアクションを使用して、ダッシュボードアプリケーションに保存された検索パネルからCSV形式のレポートを生成します。", - "xpack.reporting.deprecations.csvPanelActionDownload.message": "\"{enablePanelActionDownload}\"設定は廃止予定です。", - "xpack.reporting.deprecations.csvPanelActionDownload.title": "ダッシュボードの保存された検索パネルからCSVダウンロードを有効にする設定は廃止予定です。", "xpack.reporting.deprecations.migrateIndexIlmPolicy.manualStepOneMessage": "インデックス設定APIを使用して、すべてのレポートインデックスを更新し、\"{reportingIlmPolicy}\"ポリシーを使用します。", "xpack.reporting.deprecations.migrateIndexIlmPolicyActionMessage": "新しいレポートインデックスは\"{reportingIlmPolicy}\"がプロビジョニングしたILMポリシーによって管理されます。レポートライフサイクルを管理するには、このポリシーを編集する必要があります。この変更は、非表示のシステムインデックスパターン\"{indexPattern}\"を対象としています。", "xpack.reporting.deprecations.migrateIndexIlmPolicyActionTitle": "カスタムILMポリシーで管理されたレポートインデックスが見つかりました。", @@ -35997,15 +35973,6 @@ "xpack.searchInferenceEndpoints.actions.endpointAddedSuccess": "エンドポイントが追加されました", "xpack.searchInferenceEndpoints.actions.endpointAddedSuccessDescription": "インターフェースエンドポイント\"{endpointId}\"が追加されました。", "xpack.searchInferenceEndpoints.actions.trainedModelsStatGatherFailed": "学習済みモデル統計情報を取得できませんでした", - "xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription": "推論エンドポイントを使用すると、サードパーティサービスが提供するNLPモデルや、ELSERやE5などのElasticの組み込みモデルを使用して推論タスクを実行できます。Create Inference APIを使用して、テキスト埋め込み、入力、再ランク付けなどのタスクを設定します。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description": "E5は、密ベクトル表現を使用して、多言語のセマンティック検索を可能にするサードパーティNLPモデルです。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title": "E5 Multilingual", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription": "ELSERは、英語でのセマンティック検索向けにElasticの疎ベクトルNLPモデルです。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserTitle": "ELSER", - "xpack.searchInferenceEndpoints.addEmptyPrompt.learnHowToCreateInferenceEndpoints": "推論エンドポイントを作成する方法", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithE5": "E5 Multilingualを使用したセマンティック検索", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithElser": "ELSERを使用したセマンティック検索", - "xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel": "組み込まれたNLPモデルの詳細:", "xpack.searchInferenceEndpoints.allInferenceEndpoints.description": "推論エンドポイントは、Elasticsearchにおける機械学習モデルのデプロイと管理を合理化します。独自のエンドポイントを使用してNLPタスクを設定および管理し、AIを活用した検索を構築します。", "xpack.searchInferenceEndpoints.apiDocumentationLink": "APIドキュメント", "xpack.searchInferenceEndpoints.cancel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a1c92993682d4..e357618355f9b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11294,7 +11294,6 @@ "xpack.apm.profiling.callout.dismiss": "关闭", "xpack.apm.profiling.callout.learnMore": "了解详情", "xpack.apm.profiling.callout.title": "正在显示运行 {serviceName} 服务的主机中的分析洞见", - "xpack.apm.profiling.flamegraph.filteredLabel": "正在显示服务主机中的分析洞见", "xpack.apm.profiling.flamegraph.link": "前往 Universal Profiling 火焰图", "xpack.apm.profiling.flamegraph.noDataFound": "未找到任何数据", "xpack.apm.profiling.tabs.flamegraph": "火焰图", @@ -14779,15 +14778,9 @@ "xpack.csp.grouping.nullGroupTooltip.groupingTitle": "分组依据", "xpack.csp.integrationDashboard.noFindings.promptTitle": "结果评估状态", "xpack.csp.integrationDashboard.noFindingsPrompt.promptDescription": "正在等待要收集和索引的数据。如果此进程花费的时间长于预期,请联系我们的支持人员", - "xpack.csp.kspmIntegration.aksOption.benchmarkTitle": "CIS AKS", - "xpack.csp.kspmIntegration.aksOption.nameTitle": "AKS", - "xpack.csp.kspmIntegration.aksOption.tooltipContent": "Azure Kubernetes 服务 - 即将推出", "xpack.csp.kspmIntegration.eksOption.benchmarkTitle": "CIS EKS", "xpack.csp.kspmIntegration.eksOption.nameTitle": "EKS", "xpack.csp.kspmIntegration.eksOption.tooltipContent": "Elastic Kubernetes 服务", - "xpack.csp.kspmIntegration.gkeOption.benchmarkTitle": "CIS GKE", - "xpack.csp.kspmIntegration.gkeOption.nameTitle": "GKE", - "xpack.csp.kspmIntegration.gkeOption.tooltipContent": "Google Kubernetes Engine - 即将推出", "xpack.csp.kspmIntegration.integration.nameTitle": "Kubernetes 安全态势管理", "xpack.csp.kspmIntegration.integration.shortNameTitle": "KSPM", "xpack.csp.kspmIntegration.vanillaOption.benchmarkTitle": "CIS Kubernetes", @@ -15104,7 +15097,6 @@ "xpack.datasetQuality.types.label": "类型", "xpack.dataUsage.charts.ingestedMax": "已采集的数据", "xpack.dataUsage.charts.retainedMax": "保留在存储中的数据", - "xpack.dataUsage.metrics.filter.clearAll": "全部清除", "xpack.dataUsage.metrics.filter.dataStreams": "数据流", "xpack.dataUsage.metrics.filter.emptyMessage": "无 {filterName} 可用", "xpack.dataUsage.metrics.filter.metricTypes": "指标类型", @@ -17124,10 +17116,6 @@ "xpack.enterpriseSearch.content.connectors.deleteModal.delete.crawler.description": "您即将删除此网络爬虫:", "xpack.enterpriseSearch.content.connectors.deleteModal.syncsWarning.indexNameDescription": "此操作无法撤消。请尝试 {connectorName} 以确认。", "xpack.enterpriseSearch.content.connectors.deleteModal.title": "删除 {connectorCount} 个连接器?", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.ariaLabel": "编辑连接器描述", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.description.placeholder": "添加描述", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.ariaLabel": "编辑连接器名称", - "xpack.enterpriseSearch.content.connectors.nameAndDescription.name.placeholder": "为连接器添加名称", "xpack.enterpriseSearch.content.connectors.overview.connectorErrorCallOut.title": "您的连接器报告了错误", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.description": "此连接器将在其下次同步时创建索引,或者,您也可以使用所需设置和映射手动创建索引 {indexName}。", "xpack.enterpriseSearch.content.connectors.overview.connectorIndexDoesntExistCallOut.title": "附加的索引不存在", @@ -17947,10 +17935,7 @@ "xpack.enterpriseSearch.createConnector..title": "创建连接器", "xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged": "Elastic 托管", "xpack.enterpriseSearch.createConnector.badgeType.selfManaged": "自管型", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel": "公测版", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel": "自管型", "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text": "选择数据源", - "xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel": "技术预览", "xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel": "配置", "xpack.enterpriseSearch.createConnector.configurationStep.h4.finishUpLabel": "结束", "xpack.enterpriseSearch.createConnector.configurationStep.p.description": "您可以手动同步数据,计划重复同步或管理您的域。", @@ -20119,7 +20104,6 @@ "xpack.fleet.agentPolicyForm.newAgentPolicyFieldLabel": "新代理策略名称", "xpack.fleet.agentPolicyForm.outputOptionDisabledTypeNotSupportedText": "Fleet 服务器、Synthetics 或 APM 不支持代理集成的 {outputType} 输出。", "xpack.fleet.agentPolicyForm.outputOptionDisableOutputTypeText": "Fleet 服务器、Synthetics 或 APM 不支持代理集成的 {outputType} 输出。", - "xpack.fleet.agentPolicyForm.spaceDescription": "为此策略选择一个或多个工作区,或创建新工作区。{link}", "xpack.fleet.agentPolicyForm.spaceFieldLabel": "工作区", "xpack.fleet.agentPolicyForm.systemMonitoringText": "收集系统日志和指标", "xpack.fleet.agentPolicyForm.systemMonitoringTooltipText": "这还会添加 {system} 集成以收集系统日志和指标。", @@ -22898,7 +22882,6 @@ "xpack.idxMgmt.mappingsEditor.parameters.validations.fieldDataFrequency.numberGreaterThanOneErrorMessage": "值必须大于 1。", "xpack.idxMgmt.mappingsEditor.parameters.validations.greaterThanZeroErrorMessage": "缩放因数必须大于 0。", "xpack.idxMgmt.mappingsEditor.parameters.validations.ignoreAboveIsRequiredErrorMessage": "字符长度限制必填。", - "xpack.idxMgmt.mappingsEditor.parameters.validations.inferenceIdIsRequiredErrorMessage": "'推理 ID'必填。", "xpack.idxMgmt.mappingsEditor.parameters.validations.localeFieldRequiredErrorMessage": "指定区域设置。", "xpack.idxMgmt.mappingsEditor.parameters.validations.maxInputLengthFieldRequiredErrorMessage": "指定最大输入长度。", "xpack.idxMgmt.mappingsEditor.parameters.validations.nameIsRequiredErrorMessage": "为字段提供名称。", @@ -25866,7 +25849,6 @@ "xpack.lens.app.createVisualizationLabel": "ES|QL", "xpack.lens.app.docLoadingError": "加载已保存文档时出错", "xpack.lens.app.editLensEmbeddableLabel": "编辑可视化", - "xpack.lens.app.editVisualizationLabel": "编辑 {lang} 可视化", "xpack.lens.app.exploreDataInDiscover": "在 Discover 中浏览", "xpack.lens.app.exploreDataInDiscoverDrilldown": "在 Discover 中打开", "xpack.lens.app.exploreDataInDiscoverDrilldown.newTabConfig": "在新选项卡中打开", @@ -26025,14 +26007,9 @@ "xpack.lens.editorFrame.suggestionPanelTitle": "建议", "xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel": "请移除{dimensionsTooMany, plural, one {一个维度} other {{dimensionsTooMany} 个维度}}", "xpack.lens.editorFrame.workspaceLabel": "工作区", - "xpack.lens.embeddable.failure": "无法显示可视化", - "xpack.lens.embeddable.featureBadge.iconDescription": "{count} 个可视化{count, plural, other {修饰符}}", - "xpack.lens.embeddable.fixErrors": "在 Lens 编辑器中编辑以修复该错误", - "xpack.lens.embeddable.legacyURLConflict.shortMessage": "您遇到了 URL 冲突", - "xpack.lens.embeddable.missingTimeRangeParam.longMessage": "给定配置需要包含 timeRange 属性", - "xpack.lens.embeddable.missingTimeRangeParam.shortMessage": "缺少 timeRange 属性", - "xpack.lens.embeddable.moreErrors": "在 Lens 编辑器中编辑以查看更多错误", - "xpack.lens.embeddableDisplayName": "Lens", + "xpack.lens.featureBadge.iconDescription": "{count} 个可视化{count, plural, other {修饰符}}", + "xpack.lens.fixErrors": "在 Lens 编辑器中编辑以修复该错误", + "xpack.lens.moreErrors": "在 Lens 编辑器中编辑以查看更多错误", "xpack.lens.endValue.nearest": "最近", "xpack.lens.endValue.none": "隐藏", "xpack.lens.endValue.zero": "零", @@ -26542,7 +26519,6 @@ "xpack.lens.legacyMetric.titlePositions.bottom": "底部", "xpack.lens.legacyMetric.titlePositions.top": "顶部", "xpack.lens.legacyUrlConflict.objectNoun": "Lens 可视化", - "xpack.lens.lensSavedObjectLabel": "Lens 可视化", "xpack.lens.lineCurve.smooth": "平滑", "xpack.lens.lineCurve.step": "步骤", "xpack.lens.lineCurve.straight": "直线", @@ -34896,8 +34872,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "没有该名称的远程集群。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.breadcrumb": "Reporting", - "xpack.reporting.deprecations.csvPanelActionDownload.manualStep2": "在 Dashboard 应用程序中使用替代面板操作从已保存搜索面板生成 CSV 报告。", - "xpack.reporting.deprecations.csvPanelActionDownload.title": "在仪表板中从已保存搜索面板启用 CSV 下载的设置已弃用。", "xpack.reporting.deprecations.migrateIndexIlmPolicyActionTitle": "找到由定制 ILM 策略管理的报告索引。", "xpack.reporting.deprecations.reportingRole.forbiddenErrorCorrectiveAction": "请确保您分配有'manage_security'集群权限。", "xpack.reporting.deprecations.reportingRole.forbiddenErrorMessage": "您没有足够的权限来修复此弃用。", @@ -35435,15 +35409,6 @@ "xpack.searchInferenceEndpoints.actions.endpointAddedFailure": "终端创建失败", "xpack.searchInferenceEndpoints.actions.endpointAddedSuccess": "已添加终端", "xpack.searchInferenceEndpoints.actions.trainedModelsStatGatherFailed": "无法检索已训练模型统计信息", - "xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription": "利用推理终端,您可以使用由第三方服务提供的 NLP 模型或 ELSER 和 E5 等 Elastic 内置模型来执行推理任务。通过使用创建推理 API 来设置任务,如文本嵌入、完成、重新排名等。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description": "E5 是一个第三方 NLP 模型,它允许您通过使用密集向量表示方法来执行多语言语义搜索。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title": "E5 多语言", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription": "ELSER 是 Elastic 的英文版稀疏向量语义搜索 NLP 模型。", - "xpack.searchInferenceEndpoints.addEmptyPrompt.elserTitle": "ELSER", - "xpack.searchInferenceEndpoints.addEmptyPrompt.learnHowToCreateInferenceEndpoints": "了解如何创建推理终端", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithE5": "利用 E5 多语言版进行语义搜索", - "xpack.searchInferenceEndpoints.addEmptyPrompt.semanticSearchWithElser": "利用 ELSER 进行语义搜索", - "xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel": "详细了解内置 NLP 模型:", "xpack.searchInferenceEndpoints.allInferenceEndpoints.description": "推理终端简化了 Elasticsearch 中的 Machine Learning 模型部署和管理。使用唯一的终端来设置和管理 NLP 任务,以构建 AI 驱动式搜索。", "xpack.searchInferenceEndpoints.apiDocumentationLink": "API 文档", "xpack.searchInferenceEndpoints.cancel": "取消", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx index 1a5e3f278eb5e..8452345543145 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useBulkEditSelect } from './use_bulk_edit_select'; import { RuleTableItem } from '../../types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx index 5fe09dd8ae906..f33092c3dea9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../common/lib/kibana'; import { useCreateConnector } from './use_create_connector'; @@ -29,7 +29,7 @@ describe('useCreateConnector', () => { }); it('executes correctly', async () => { - const { result, waitForNextUpdate } = renderHook(() => useCreateConnector()); + const { result } = renderHook(() => useCreateConnector()); act(() => { result.current.createConnector({ @@ -40,10 +40,10 @@ describe('useCreateConnector', () => { }); }); - await waitForNextUpdate(); - - expect(useKibanaMock().services.http.post).toHaveBeenCalledWith('/api/actions/connector', { - body: '{"name":"test","config":{},"secrets":{},"connector_type_id":".test"}', - }); + await waitFor(() => + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith('/api/actions/connector', { + body: '{"name":"test","config":{},"secrets":{},"connector_type_id":".test"}', + }) + ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx index 4b93900ea6b4e..a381296e6bc91 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../common/lib/kibana'; import { useExecuteConnector } from './use_execute_connector'; @@ -29,17 +29,17 @@ describe('useExecuteConnector', () => { }); it('executes correctly', async () => { - const { result, waitForNextUpdate } = renderHook(() => useExecuteConnector()); + const { result } = renderHook(() => useExecuteConnector()); act(() => { result.current.executeConnector({ connectorId: 'test-id', params: {} }); }); - await waitForNextUpdate(); - - expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( - '/api/actions/connector/test-id/_execute', - { body: '{"params":{}}' } + await waitFor(() => + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( + '/api/actions/connector/test-id/_execute', + { body: '{"params":{}}' } + ) ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx index 4b65e355b9c86..231b9b9be49d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useGetQueryDelaySettings } from './use_get_query_delay_settings'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_license.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_license.test.ts index 1b2fc7784f4e3..ec203e4bdacf8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_license.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_license.test.ts @@ -6,7 +6,7 @@ */ import { BehaviorSubject } from 'rxjs'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLicense } from './use_license'; import { useKibana } from '../../common/lib/kibana'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.test.ts index e56c8aa1348b9..92c1e3bc5ca69 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { ValidFeatureId } from '@kbn/rule-data-utils'; import { useKibana } from '../../common/lib/kibana'; import { @@ -34,7 +34,7 @@ describe('useLoadAlertSummary', () => { ...mockedAlertSummaryResponse, }); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useLoadAlertSummary({ featureIds, timeRange: mockedAlertSummaryTimeRange, @@ -49,7 +49,7 @@ describe('useLoadAlertSummary', () => { }, }); - await waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); const { alertSummary, error } = result.current; expect(alertSummary).toEqual({ @@ -70,7 +70,7 @@ describe('useLoadAlertSummary', () => { ...mockedAlertSummaryResponse, }); - const { waitForNextUpdate } = renderHook(() => + renderHook(() => useLoadAlertSummary({ featureIds, timeRange: mockedAlertSummaryTimeRange, @@ -78,8 +78,6 @@ describe('useLoadAlertSummary', () => { }) ); - await waitForNextUpdate(); - const body = JSON.stringify({ fixed_interval: fixedInterval, gte: utcFrom, @@ -87,11 +85,14 @@ describe('useLoadAlertSummary', () => { featureIds, filter: [filter], }); - expect(mockedPostAPI).toHaveBeenCalledWith( - '/internal/rac/alerts/_alert_summary', - expect.objectContaining({ - body, - }) + + await waitFor(() => + expect(mockedPostAPI).toHaveBeenCalledWith( + '/internal/rac/alerts/_alert_summary', + expect.objectContaining({ + body, + }) + ) ); }); @@ -99,15 +100,13 @@ describe('useLoadAlertSummary', () => { const error = new Error('Fetch Alert Summary Failed'); mockedPostAPI.mockRejectedValueOnce(error); - const { result, waitForNextUpdate } = renderHook(() => + const { result } = renderHook(() => useLoadAlertSummary({ featureIds, timeRange: mockedAlertSummaryTimeRange, }) ); - await waitForNextUpdate(); - - expect(result.current.error).toMatch(error.message); + await waitFor(() => expect(result.current.error).toMatch(error.message)); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.tsx index c0e143119f6fa..e1cb5f6f4f5ee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.tsx @@ -5,13 +5,12 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor, renderHook } from '@testing-library/react'; import { useLoadRuleAggregationsQuery as useLoadRuleAggregations } from './use_load_rule_aggregations_query'; import { RuleStatus } from '../../types'; import { useKibana } from '../../common/lib/kibana'; import { IToasts } from '@kbn/core-notifications-browser'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { waitFor } from '@testing-library/react'; jest.mock('../../common/lib/kibana'); jest.mock('../lib/rule_api/aggregate_kuery_filter', () => ({ @@ -75,7 +74,7 @@ describe('useLoadRuleAggregations', () => { refresh: undefined, }; - const { rerender, result, waitForNextUpdate } = renderHook( + const { rerender, result } = renderHook( () => { return useLoadRuleAggregations(params); }, @@ -83,20 +82,21 @@ describe('useLoadRuleAggregations', () => { ); rerender(); - await waitForNextUpdate(); - expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith( - expect.objectContaining({ - searchText: '', - typesFilter: [], - actionTypesFilter: [], - ruleExecutionStatusesFilter: [], - ruleLastRunOutcomesFilter: [], - ruleStatusesFilter: [], - tagsFilter: [], - }) - ); - expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + await waitFor(() => { + expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith( + expect.objectContaining({ + searchText: '', + typesFilter: [], + actionTypesFilter: [], + ruleExecutionStatusesFilter: [], + ruleLastRunOutcomesFilter: [], + ruleStatusesFilter: [], + tagsFilter: [], + }) + ); + expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + }); }); it('should call loadRuleAggregation API with params and handle result', async () => { @@ -115,28 +115,26 @@ describe('useLoadRuleAggregations', () => { refresh: undefined, }; - const { rerender, result, waitForNextUpdate } = renderHook( - () => useLoadRuleAggregations(params), - { - wrapper, - } - ); + const { rerender, result } = renderHook(() => useLoadRuleAggregations(params), { + wrapper, + }); rerender(); - await waitForNextUpdate(); - expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith( - expect.objectContaining({ - searchText: 'test', - typesFilter: ['type1', 'type2'], - actionTypesFilter: ['action1', 'action2'], - ruleExecutionStatusesFilter: ['status1', 'status2'], - ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[], - tagsFilter: ['tag1', 'tag2'], - ruleLastRunOutcomesFilter: ['outcome1', 'outcome2'], - }) - ); - expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + await waitFor(() => { + expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith( + expect.objectContaining({ + searchText: 'test', + typesFilter: ['type1', 'type2'], + actionTypesFilter: ['action1', 'action2'], + ruleExecutionStatusesFilter: ['status1', 'status2'], + ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[], + tagsFilter: ['tag1', 'tag2'], + ruleLastRunOutcomesFilter: ['outcome1', 'outcome2'], + }) + ); + expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus); + }); }); it('should call onError if API fails', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.tsx index 8cc07c87e2411..3c87b04cb62e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor, renderHook } from '@testing-library/react'; import { useLoadRulesQuery as useLoadRules } from './use_load_rules_query'; import { RuleExecutionStatusErrorReasons, @@ -15,7 +15,6 @@ import { RuleStatus } from '../../types'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useKibana } from '../../common/lib/kibana'; import { IToasts } from '@kbn/core-notifications-browser'; -import { waitFor } from '@testing-library/react'; jest.mock('../../common/lib/kibana'); jest.mock('../lib/rule_api/rules_kuery_filter', () => ({ @@ -282,7 +281,7 @@ describe('useLoadRules', () => { sort: { field: 'name', direction: 'asc' }, }; - const { result, waitForNextUpdate, rerender } = renderHook(() => useLoadRules(params), { + const { result, rerender } = renderHook(() => useLoadRules(params), { wrapper, }); @@ -291,11 +290,10 @@ describe('useLoadRules', () => { expect(result.current.rulesState.isLoading).toBeTruthy(); rerender(); - await waitForNextUpdate(); + await waitFor(() => expect(result.current.rulesState.isLoading).toBeFalsy()); expect(result.current.rulesState.initialLoad).toBeFalsy(); expect(result.current.hasData).toBeTruthy(); - expect(result.current.rulesState.isLoading).toBeFalsy(); expect(onPage).toBeCalledTimes(0); expect(loadRulesWithKueryFilter).toBeCalledWith( @@ -339,29 +337,29 @@ describe('useLoadRules', () => { sort: { field: 'name', direction: 'asc' }, }; - const { waitForNextUpdate, rerender } = renderHook(() => useLoadRules(params), { + const { rerender } = renderHook(() => useLoadRules(params), { wrapper, }); rerender(); - await waitForNextUpdate(); - - expect(loadRulesWithKueryFilter).toBeCalledWith( - expect.objectContaining({ - page: { - index: 0, - size: 25, - }, - searchText: 'test', - typesFilter: ['type1', 'type2'], - actionTypesFilter: ['action1', 'action2'], - ruleExecutionStatusesFilter: ['status1', 'status2'], - ruleLastRunOutcomesFilter: ['outcome1', 'outcome2'], - ruleParamsFilter: {}, - ruleStatusesFilter: ['enabled', 'snoozed'], - tagsFilter: ['tag1', 'tag2'], - sort: { field: 'name', direction: 'asc' }, - }) + await waitFor(() => + expect(loadRulesWithKueryFilter).toBeCalledWith( + expect.objectContaining({ + page: { + index: 0, + size: 25, + }, + searchText: 'test', + typesFilter: ['type1', 'type2'], + actionTypesFilter: ['action1', 'action2'], + ruleExecutionStatusesFilter: ['status1', 'status2'], + ruleLastRunOutcomesFilter: ['outcome1', 'outcome2'], + ruleParamsFilter: {}, + ruleStatusesFilter: ['enabled', 'snoozed'], + tagsFilter: ['tag1', 'tag2'], + sort: { field: 'name', direction: 'asc' }, + }) + ) ); }); @@ -391,7 +389,7 @@ describe('useLoadRules', () => { sort: { field: 'name', direction: 'asc' }, }; - const { rerender, waitForNextUpdate } = renderHook( + const { rerender } = renderHook( () => { return useLoadRules(params); }, @@ -399,12 +397,12 @@ describe('useLoadRules', () => { ); rerender(); - await waitForNextUpdate(); - - expect(onPage).toHaveBeenCalledWith({ - index: 0, - size: 25, - }); + await waitFor(() => + expect(onPage).toHaveBeenCalledWith({ + index: 0, + size: 25, + }) + ); }); it('should call onError if API fails', async () => { @@ -459,16 +457,14 @@ describe('useLoadRules', () => { sort: { field: 'name', direction: 'asc' }, }; - const { rerender, result, waitForNextUpdate } = renderHook(() => useLoadRules(params), { + const { rerender, result } = renderHook(() => useLoadRules(params), { wrapper, }); expect(result.current.hasData).toBeFalsy(); rerender(); - await waitForNextUpdate(); - - expect(result.current.hasData).toBeFalsy(); + await waitFor(() => expect(result.current.hasData).toBeFalsy()); }); it('hasData should be false, if there is rule types filter and no rules with hasDefaultRuleTypesFiltersOn = true', async () => { @@ -494,16 +490,14 @@ describe('useLoadRules', () => { hasDefaultRuleTypesFiltersOn: true, }; - const { rerender, result, waitForNextUpdate } = renderHook(() => useLoadRules(params), { + const { rerender, result } = renderHook(() => useLoadRules(params), { wrapper, }); expect(result.current.hasData).toBeFalsy(); rerender(); - await waitForNextUpdate(); - - expect(result.current.hasData).toBeFalsy(); + await waitFor(() => expect(result.current.hasData).toBeFalsy()); }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags_query.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags_query.test.tsx index 081538d1432ef..3aa1bcbf07b2c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags_query.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags_query.test.tsx @@ -5,12 +5,11 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor, renderHook } from '@testing-library/react'; import { useLoadTagsQuery } from './use_load_tags_query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useKibana } from '../../common/lib/kibana'; import { IToasts } from '@kbn/core-notifications-browser'; -import { waitFor } from '@testing-library/react'; const MOCK_TAGS = ['a', 'b', 'c']; @@ -53,7 +52,7 @@ describe('useLoadTagsQuery', () => { }); it('should call loadRuleTags API and handle result', async () => { - const { rerender, result, waitForNextUpdate } = renderHook( + const { rerender, result } = renderHook( () => useLoadTagsQuery({ enabled: true, @@ -67,18 +66,18 @@ describe('useLoadTagsQuery', () => { ); rerender(); - await waitForNextUpdate(); - - expect(loadRuleTags).toHaveBeenLastCalledWith( - expect.objectContaining({ - search: 'test', - perPage: 50, - page: 1, - }) - ); + await waitFor(() => { + expect(loadRuleTags).toHaveBeenLastCalledWith( + expect.objectContaining({ + search: 'test', + perPage: 50, + page: 1, + }) + ); - expect(result.current.tags).toEqual(MOCK_TAGS); - expect(result.current.hasNextPage).toEqual(false); + expect(result.current.tags).toEqual(MOCK_TAGS); + expect(result.current.hasNextPage).toEqual(false); + }); }); it('should support pagination', async () => { @@ -88,7 +87,7 @@ describe('useLoadTagsQuery', () => { perPage: 5, total: 10, }); - const { rerender, result, waitForNextUpdate } = renderHook( + const { rerender, result } = renderHook( () => useLoadTagsQuery({ enabled: true, @@ -101,17 +100,17 @@ describe('useLoadTagsQuery', () => { ); rerender(); - await waitForNextUpdate(); - - expect(loadRuleTags).toHaveBeenLastCalledWith( - expect.objectContaining({ - perPage: 5, - page: 1, - }) - ); + await waitFor(() => { + expect(loadRuleTags).toHaveBeenLastCalledWith( + expect.objectContaining({ + perPage: 5, + page: 1, + }) + ); - expect(result.current.tags).toEqual(['a', 'b', 'c', 'd', 'e']); - expect(result.current.hasNextPage).toEqual(true); + expect(result.current.tags).toEqual(['a', 'b', 'c', 'd', 'e']); + expect(result.current.hasNextPage).toEqual(true); + }); loadRuleTags.mockResolvedValue({ data: ['a', 'b', 'c', 'd', 'e'], @@ -119,6 +118,7 @@ describe('useLoadTagsQuery', () => { perPage: 5, total: 10, }); + result.current.fetchNextPage(); expect(loadRuleTags).toHaveBeenLastCalledWith( @@ -129,9 +129,7 @@ describe('useLoadTagsQuery', () => { ); rerender(); - await waitForNextUpdate(); - - expect(result.current.hasNextPage).toEqual(false); + await waitFor(() => expect(result.current.hasNextPage).toEqual(false)); }); it('should support pagination when there are no tags', async () => { @@ -142,7 +140,7 @@ describe('useLoadTagsQuery', () => { total: 0, }); - const { rerender, result, waitForNextUpdate } = renderHook( + const { rerender, result } = renderHook( () => useLoadTagsQuery({ enabled: true, @@ -155,17 +153,17 @@ describe('useLoadTagsQuery', () => { ); rerender(); - await waitForNextUpdate(); - - expect(loadRuleTags).toHaveBeenLastCalledWith( - expect.objectContaining({ - perPage: 5, - page: 1, - }) - ); + await waitFor(() => { + expect(loadRuleTags).toHaveBeenLastCalledWith( + expect.objectContaining({ + perPage: 5, + page: 1, + }) + ); - expect(result.current.tags).toEqual([]); - expect(result.current.hasNextPage).toEqual(false); + expect(result.current.tags).toEqual([]); + expect(result.current.hasNextPage).toEqual(false); + }); }); it('should call onError if API fails', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx index 1f2552511de00..161f1564b1dd7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../common/lib/kibana'; import { useSubAction, UseSubActionParams } from './use_sub_action'; @@ -30,102 +30,96 @@ describe('useSubAction', () => { }); it('init', async () => { - const { result, waitForNextUpdate } = renderHook(() => useSubAction(params)); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - isLoading: false, - response: {}, - error: null, - }); + const { result } = renderHook(() => useSubAction(params)); + await waitFor(() => + expect(result.current).toEqual({ + isLoading: false, + response: {}, + error: null, + }) + ); }); it('executes the sub action correctly', async () => { - const { waitForNextUpdate } = renderHook(() => useSubAction(params)); - await waitForNextUpdate(); - - expect(mockHttpPost).toHaveBeenCalledWith('/api/actions/connector/test-id/_execute', { - body: '{"params":{"subAction":"test","subActionParams":{"foo":"bar"}}}', - signal: new AbortController().signal, - }); + renderHook(() => useSubAction(params)); + await waitFor(() => + expect(mockHttpPost).toHaveBeenCalledWith('/api/actions/connector/test-id/_execute', { + body: '{"params":{"subAction":"test","subActionParams":{"foo":"bar"}}}', + signal: new AbortController().signal, + }) + ); }); it('executes sub action if subAction parameter changes', async () => { - const { rerender, waitForNextUpdate } = renderHook(useSubAction, { initialProps: params }); - await waitForNextUpdate(); - - expect(mockHttpPost).toHaveBeenCalledTimes(1); + const { rerender } = renderHook(useSubAction, { initialProps: params }); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(1)); await act(async () => { rerender({ ...params, subAction: 'test-2' }); - await waitForNextUpdate(); }); - expect(mockHttpPost).toHaveBeenCalledTimes(2); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(2)); }); it('executes sub action if connectorId parameter changes', async () => { - const { rerender, waitForNextUpdate } = renderHook(useSubAction, { initialProps: params }); - await waitForNextUpdate(); - - expect(mockHttpPost).toHaveBeenCalledTimes(1); + const { rerender } = renderHook(useSubAction, { initialProps: params }); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(1)); await act(async () => { rerender({ ...params, connectorId: 'test-id-2' }); - await waitForNextUpdate(); }); - expect(mockHttpPost).toHaveBeenCalledTimes(2); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(2)); }); it('returns memoized response if subActionParams changes but values are equal', async () => { - const { result, rerender, waitForNextUpdate } = renderHook(useSubAction, { + const { result, rerender } = renderHook(useSubAction, { initialProps: { ...params, subActionParams: { foo: 'bar' } }, }); - await waitForNextUpdate(); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(1)); - expect(mockHttpPost).toHaveBeenCalledTimes(1); const previous = result.current; await act(async () => { rerender({ ...params, subActionParams: { foo: 'bar' } }); - await waitForNextUpdate(); }); - expect(result.current.response).toBe(previous.response); - expect(mockHttpPost).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(result.current.response).toBe(previous.response); + expect(mockHttpPost).toHaveBeenCalledTimes(1); + }); }); it('executes sub action if subActionParams changes and values are not equal', async () => { - const { result, rerender, waitForNextUpdate } = renderHook(useSubAction, { + const { result, rerender } = renderHook(useSubAction, { initialProps: { ...params, subActionParams: { foo: 'bar' } }, }); - await waitForNextUpdate(); + await waitFor(() => expect(mockHttpPost).toHaveBeenCalledTimes(1)); - expect(mockHttpPost).toHaveBeenCalledTimes(1); const previous = result.current; await act(async () => { rerender({ ...params, subActionParams: { foo: 'baz' } }); - await waitForNextUpdate(); }); - expect(result.current.response).not.toBe(previous.response); - expect(mockHttpPost).toHaveBeenCalledTimes(2); + await waitFor(() => { + expect(result.current.response).not.toBe(previous.response); + expect(mockHttpPost).toHaveBeenCalledTimes(2); + }); }); it('returns an error correctly', async () => { const error = new Error('error executing'); mockHttpPost.mockRejectedValueOnce(error); - const { result, waitForNextUpdate } = renderHook(() => useSubAction(params)); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - isLoading: false, - response: undefined, - error, - }); + const { result } = renderHook(() => useSubAction(params)); + await waitFor(() => + expect(result.current).toEqual({ + isLoading: false, + response: undefined, + error, + }) + ); }); it('should abort on unmount', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx index c5f61885adddc..e1a04ea929b59 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook, act } from '@testing-library/react'; import { useUpdateRuleSettings } from './use_update_rules_settings'; const mockAddDanger = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.test.tsx index 3fb8207e45ef3..440e24017fab6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.test.tsx @@ -209,4 +209,64 @@ describe('action_type_form', () => { expect(setActionParamsProperty).toHaveBeenCalledWith('my-key', 'my-value', 1); }); + + describe('licensing', () => { + const actionTypeIndexDefaultWithLicensing = { + ...actionTypeIndexDefault, + '.test-system-action': { + ...actionTypeIndexDefault['.test-system-action'], + enabledInLicense: false, + minimumLicenseRequired: 'platinum' as const, + }, + }; + + beforeEach(() => { + const actionType = actionTypeRegistryMock.createMockActionTypeModel({ + id: '.test-system-action-with-license', + iconClass: 'test', + selectMessage: 'test', + validateParams: (): Promise<GenericValidationResult<unknown>> => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + defaultActionParams: { + dedupKey: 'test', + eventAction: 'resolve', + }, + isSystemActionType: true, + }); + + actionTypeRegistry.get.mockReturnValue(actionType); + + jest.clearAllMocks(); + }); + + it('should render the licensing message if the user does not have the sufficient license', async () => { + render( + <I18nProvider> + <SystemActionTypeForm + actionConnector={actionConnector} + actionItem={actionItem} + connectors={connectors} + onDeleteAction={jest.fn()} + setActionParamsProperty={jest.fn()} + index={1} + actionTypesIndex={actionTypeIndexDefaultWithLicensing} + actionTypeRegistry={actionTypeRegistry} + messageVariables={{ context: [], state: [], params: [] }} + summaryMessageVariables={{ context: [], state: [], params: [] }} + producerId={AlertConsumers.INFRASTRUCTURE} + featureId={AlertConsumers.INFRASTRUCTURE} + ruleTypeId={'test'} + /> + </I18nProvider> + ); + + expect( + await screen.findByText('This feature requires a Platinum license.') + ).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx index d869449bcd929..36da1b594433d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx @@ -28,6 +28,7 @@ import { isEmpty, partition, some } from 'lodash'; import { ActionVariable, RuleActionParam } from '@kbn/alerting-plugin/common'; import { ActionGroupWithMessageVariables } from '@kbn/triggers-actions-ui-types'; import { transformActionVariables } from '@kbn/alerts-ui-shared/src/action_variables/transforms'; +import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared/src/rule_form/utils/check_action_type_enabled'; import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../translations'; import { IErrorObject, @@ -167,8 +168,12 @@ export const SystemActionTypeForm = ({ }; const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields; + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex[actionConnector.actionTypeId], + [] + ); - const accordionContent = ( + const accordionContent = checkEnabledResult.isEnabled ? ( <> <EuiSplitPanel.Inner color="plain"> {ParamsFieldsComponent ? ( @@ -212,6 +217,8 @@ export const SystemActionTypeForm = ({ ) : null} </EuiSplitPanel.Inner> </> + ) : ( + checkEnabledResult.messageCard ); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/hooks/use_rule_type_ids_by_feature_id.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/hooks/use_rule_type_ids_by_feature_id.test.ts index 3fd8ec70d9ff8..03f1f66ede9fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/hooks/use_rule_type_ids_by_feature_id.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/hooks/use_rule_type_ids_by_feature_id.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks/dom'; +import { renderHook } from '@testing-library/react'; import { useRuleTypeIdsByFeatureId } from './use_rule_type_ids_by_feature_id'; import { ruleTypesIndex } from '../../../mock/rule_types_index'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../../constants'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/use_case_view_navigation.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/use_case_view_navigation.test.ts index 904a8cae4eec7..6b1a14c0f7d2d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/use_case_view_navigation.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cases/use_case_view_navigation.test.ts @@ -6,7 +6,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer } from '../../test_utils'; import { useCaseViewNavigation } from './use_case_view_navigation'; @@ -25,8 +25,8 @@ describe('useCaseViewNavigation', () => { useKibanaMock().services.application.navigateToApp = navigateToApp; }); - it('calls navigateToApp with correct arguments', () => { - const { result, waitFor } = renderHook(() => useCaseViewNavigation(), { + it('calls navigateToApp with correct arguments', async () => { + const { result } = renderHook(() => useCaseViewNavigation(), { wrapper: appMockRender.AppWrapper, }); @@ -34,7 +34,7 @@ describe('useCaseViewNavigation', () => { result.current.navigateToCaseView({ caseId: 'test-id' }); }); - waitFor(() => { + await waitFor(() => { expect(navigateToApp).toHaveBeenCalledWith('testAppId', { deepLinkId: 'cases', path: '/test-id', @@ -42,8 +42,8 @@ describe('useCaseViewNavigation', () => { }); }); - it('calls navigateToApp with correct arguments and bypass current app id', () => { - const { result, waitFor } = renderHook(() => useCaseViewNavigation('superAppId'), { + it('calls navigateToApp with correct arguments and bypass current app id', async () => { + const { result } = renderHook(() => useCaseViewNavigation('superAppId'), { wrapper: appMockRender.AppWrapper, }); @@ -51,7 +51,7 @@ describe('useCaseViewNavigation', () => { result.current.navigateToCaseView({ caseId: 'test-id' }); }); - waitFor(() => { + await waitFor(() => { expect(navigateToApp).toHaveBeenCalledWith('superAppId', { deepLinkId: 'cases', path: '/test-id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx index 8d65532c5f10a..2718ccd8ca88e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import * as api from '../apis/get_rules_muted_alerts'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../../../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils'; import { useGetMutedAlerts } from './use_get_muted_alerts'; @@ -31,12 +30,10 @@ describe('useGetMutedAlerts', () => { it('calls the api when invoked with the correct parameters', async () => { const muteAlertInstanceSpy = jest.spyOn(api, 'getMutedAlerts'); - const { waitForNextUpdate } = renderHook(() => useGetMutedAlerts(ruleIds), { + renderHook(() => useGetMutedAlerts(ruleIds), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - await waitFor(() => { expect(muteAlertInstanceSpy).toHaveBeenCalledWith( expect.anything(), @@ -53,18 +50,16 @@ describe('useGetMutedAlerts', () => { wrapper: appMockRender.AppWrapper, }); - expect(spy).not.toHaveBeenCalled(); + await waitFor(() => expect(spy).not.toHaveBeenCalled()); }); it('shows a toast error when the api returns an error', async () => { const spy = jest.spyOn(api, 'getMutedAlerts').mockRejectedValue(new Error('An error')); - const { waitForNextUpdate } = renderHook(() => useGetMutedAlerts(ruleIds), { + renderHook(() => useGetMutedAlerts(ruleIds), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - await waitFor(() => { expect(spy).toHaveBeenCalled(); expect(addErrorMock).toHaveBeenCalled(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx index f7e8b94aa2e66..74d93a0504ca7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks/dom'; import * as api from '../../../../lib/rule_api/mute_alert'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../../../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils'; import { useMuteAlert } from './use_mute_alert'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx index d24a481ba30b8..178dc5bb6ed9b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks/dom'; import * as api from '../../../../lib/rule_api/unmute_alert'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../../../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils'; import { useUnmuteAlert } from './use_unmute_alert'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx index d84909e746f27..0f136e15156d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useBulkActions, useBulkAddToCaseActions, useBulkUntrackActions } from './use_bulk_actions'; import { AppMockRenderer, createAppMockRenderer } from '../../test_utils'; import { createCasesServiceMock } from '../index.mock'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx index b4598f56c02f2..e79955715d4ee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import * as api from './apis/bulk_get_cases'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { useKibana } from '../../../../common/lib/kibana'; import { useBulkGetCases } from './use_bulk_get_cases'; import { AppMockRenderer, createAppMockRenderer } from '../../test_utils'; @@ -35,18 +34,18 @@ describe('useBulkGetCases', () => { const spy = jest.spyOn(api, 'bulkGetCases'); spy.mockResolvedValue(response); - const { waitForNextUpdate } = renderHook(() => useBulkGetCases(['case-1'], true), { + renderHook(() => useBulkGetCases(['case-1'], true), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - - expect(spy).toHaveBeenCalledWith( - expect.anything(), - { - ids: ['case-1'], - }, - expect.any(AbortSignal) + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + expect.anything(), + { + ids: ['case-1'], + }, + expect.any(AbortSignal) + ) ); }); @@ -58,18 +57,16 @@ describe('useBulkGetCases', () => { wrapper: appMockRender.AppWrapper, }); - expect(spy).not.toHaveBeenCalled(); + await waitFor(() => expect(spy).not.toHaveBeenCalled()); }); it('shows a toast error when the api return an error', async () => { const spy = jest.spyOn(api, 'bulkGetCases').mockRejectedValue(new Error('An error')); - const { waitForNextUpdate } = renderHook(() => useBulkGetCases(['case-1'], true), { + renderHook(() => useBulkGetCases(['case-1'], true), { wrapper: appMockRender.AppWrapper, }); - await waitForNextUpdate(); - await waitFor(() => { expect(spy).toHaveBeenCalledWith( expect.anything(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_maintenance_windows.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_maintenance_windows.test.ts index b1f8a65e16037..7a2ffd32ad2c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_maintenance_windows.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_maintenance_windows.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common'; import * as api from './apis/bulk_get_maintenance_windows'; import { coreMock } from '@kbn/core/public/mocks'; @@ -96,7 +95,7 @@ describe('useBulkGetMaintenanceWindows', () => { const spy = jest.spyOn(api, 'bulkGetMaintenanceWindows'); spy.mockResolvedValue(response); - const { waitForNextUpdate, result } = renderHook( + const { result } = renderHook( () => useBulkGetMaintenanceWindows({ ids: ['test-id'], @@ -107,9 +106,7 @@ describe('useBulkGetMaintenanceWindows', () => { } ); - await waitForNextUpdate(); - - expect(result.current.data?.get('test-id')).toEqual(mockMaintenanceWindow); + await waitFor(() => expect(result.current.data?.get('test-id')).toEqual(mockMaintenanceWindow)); expect(spy).toHaveBeenCalledWith({ http: expect.anything(), @@ -132,7 +129,7 @@ describe('useBulkGetMaintenanceWindows', () => { } ); - expect(spy).not.toHaveBeenCalled(); + await waitFor(() => expect(spy).not.toHaveBeenCalled()); }); it('does not call the api if license is not platinum', async () => { @@ -152,7 +149,7 @@ describe('useBulkGetMaintenanceWindows', () => { } ); - expect(spy).not.toHaveBeenCalled(); + await waitFor(() => expect(spy).not.toHaveBeenCalled()); }); it('does not call the api if capabilities are not adequate', async () => { @@ -177,7 +174,7 @@ describe('useBulkGetMaintenanceWindows', () => { } ); - expect(spy).not.toHaveBeenCalled(); + await waitFor(() => expect(spy).not.toHaveBeenCalled()); }); it('shows a toast error when the api return an error', async () => { @@ -185,7 +182,7 @@ describe('useBulkGetMaintenanceWindows', () => { .spyOn(api, 'bulkGetMaintenanceWindows') .mockRejectedValue(new Error('An error')); - const { waitForNextUpdate } = renderHook( + renderHook( () => useBulkGetMaintenanceWindows({ ids: ['test-id'], @@ -196,8 +193,6 @@ describe('useBulkGetMaintenanceWindows', () => { } ); - await waitForNextUpdate(); - await waitFor(() => { expect(spy).toHaveBeenCalledWith({ http: expect.anything(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_columns/use_columns.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_columns/use_columns.test.tsx index fb79f87162bcb..d6ecf3d9ab20d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_columns/use_columns.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_columns/use_columns.test.tsx @@ -9,12 +9,12 @@ import React, { FunctionComponent } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { BrowserFields } from '@kbn/alerting-types'; import { testQueryClientConfig } from '@kbn/alerts-ui-shared/src/common/test_utils/test_query_client_config'; import { fetchAlertsFields } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_fields'; -import { useColumns, UseColumnsArgs, UseColumnsResp } from './use_columns'; +import { useColumns } from './use_columns'; import { AlertsTableStorage } from '../../alerts_table_state'; import { createStartServicesMock } from '../../../../../common/lib/kibana/kibana_react.mock'; import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context'; @@ -151,10 +151,7 @@ describe('useColumns', () => { test('onColumnResize', async () => { const localDefaultColumns = [...defaultColumns]; const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(localDefaultColumns); - const { result, rerender } = renderHook< - React.PropsWithChildren<UseColumnsArgs>, - UseColumnsResp - >( + const { result, rerender } = renderHook( () => useColumns({ defaultColumns, @@ -186,7 +183,7 @@ describe('useColumns', () => { test('check if initial width for the last column does not exist', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -211,7 +208,7 @@ describe('useColumns', () => { const alertsFields = { testField: { name: 'testField', type: 'string', searchable: true, aggregatable: true }, }; - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ alertsFields, @@ -231,7 +228,7 @@ describe('useColumns', () => { describe('visibleColumns', () => { test('hide all columns with onChangeVisibleColumns', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -253,7 +250,7 @@ describe('useColumns', () => { test('show all columns with onChangeVisibleColumns', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -282,7 +279,7 @@ describe('useColumns', () => { test('should populate visibleColumns correctly', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -300,10 +297,7 @@ describe('useColumns', () => { test('should change visibleColumns if provided defaultColumns change', async () => { let localDefaultColumns = [...defaultColumns]; let localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(localDefaultColumns); - const { result, rerender } = renderHook< - React.PropsWithChildren<UseColumnsArgs>, - UseColumnsResp - >( + const { result, rerender } = renderHook( () => useColumns({ defaultColumns: localDefaultColumns, @@ -340,10 +334,7 @@ describe('useColumns', () => { describe('columns', () => { test('should changes the column list when defaultColumns has been updated', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result, waitFor } = renderHook< - React.PropsWithChildren<UseColumnsArgs>, - UseColumnsResp - >( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -362,7 +353,7 @@ describe('useColumns', () => { describe('onToggleColumns', () => { test('should update the list of columns when on Toggle Columns is called', () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -383,7 +374,7 @@ describe('useColumns', () => { test('should update the list of visible columns when onToggleColumn is called', async () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -412,7 +403,7 @@ describe('useColumns', () => { test('should update the column details in the storage when onToggleColumn is called', () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, @@ -445,7 +436,7 @@ describe('useColumns', () => { describe('onResetColumns', () => { test('should restore visible columns defaults', () => { const localStorageAlertsTable = getStorageAlertsTableByDefaultColumns(defaultColumns); - const { result } = renderHook<React.PropsWithChildren<UseColumnsArgs>, UseColumnsResp>( + const { result } = renderHook( () => useColumns({ defaultColumns, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts index 24bddbe90ec59..b53855f991c59 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts @@ -5,7 +5,8 @@ * 2.0. */ import { usePagination } from './use_pagination'; -import { renderHook, act } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; describe('usePagination', () => { const onPageChange = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_sorting.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_sorting.test.ts index 092ec0ed0eb43..95efe7c9c8c5a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_sorting.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_sorting.test.ts @@ -5,7 +5,8 @@ * 2.0. */ import { useSorting } from './use_sorting'; -import { renderHook, act } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; describe('useSorting', () => { const onSortChange = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/hooks/use_rules_list_filter_store.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/hooks/use_rules_list_filter_store.test.tsx index 89b5e56aa33dd..ee2a8637b9912 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/hooks/use_rules_list_filter_store.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/hooks/use_rules_list_filter_store.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import * as useLocalStorage from 'react-use/lib/useLocalStorage'; import { useRulesListFilterStore } from './use_rules_list_filter_store'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.test.tsx index ad623631fe85e..c3afafd716054 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_column_selector.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; +import { render, renderHook } from '@testing-library/react'; import React from 'react'; import { RulesListColumns, diff --git a/x-pack/test/api_integration/apis/cases/common/roles.ts b/x-pack/test/api_integration/apis/cases/common/roles.ts index 21ad6943ba0df..c50076b301f7c 100644 --- a/x-pack/test/api_integration/apis/cases/common/roles.ts +++ b/x-pack/test/api_integration/apis/cases/common/roles.ts @@ -136,6 +136,56 @@ export const secCasesV2All: Role = { }, }; +export const secCasesV2NoReopenWithCreateComment: Role = { + name: 'sec_cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV2: ['read', 'update', 'create', 'delete', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secCasesV2NoCreateCommentWithReopen: Role = { + name: 'sec_cases_v2_create_comment_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV2: ['read', 'update', 'create', 'delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + export const secAllSpace1: Role = { name: 'sec_all_role_space1_api_int', privileges: { @@ -434,6 +484,56 @@ export const casesV2All: Role = { }, }; +export const casesV2NoReopenWithCreateComment: Role = { + name: 'cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV2: ['read', 'update', 'create', 'delete', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const casesV2NoCreateCommentWithReopen: Role = { + name: 'cases_v2_no_create_comment_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV2: ['read', 'update', 'create', 'delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const casesRead: Role = { name: 'cases_read_role_api_int', privileges: { @@ -583,6 +683,56 @@ export const obsCasesV2All: Role = { }, }; +export const obsCasesV2NoReopenWithCreateComment: Role = { + name: 'obs_cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV2: ['read', 'update', 'create', 'delete', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const obsCasesV2NoCreateCommentWithReopen: Role = { + name: 'obs_cases_v2_no_create_comment_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV2: ['read', 'update', 'create', 'delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const obsCasesRead: Role = { name: 'obs_cases_read_role_api_int', privileges: { @@ -613,6 +763,8 @@ export const roles = [ secAllCasesNoDelete, secAll, secCasesV2All, + secCasesV2NoReopenWithCreateComment, + secCasesV2NoCreateCommentWithReopen, secAllSpace1, secAllCasesRead, secAllCasesNone, @@ -625,11 +777,15 @@ export const roles = [ casesNoDelete, casesAll, casesV2All, + casesV2NoReopenWithCreateComment, + casesV2NoCreateCommentWithReopen, casesRead, obsCasesOnlyDelete, obsCasesOnlyReadDelete, obsCasesNoDelete, obsCasesAll, obsCasesV2All, + obsCasesV2NoReopenWithCreateComment, + obsCasesV2NoCreateCommentWithReopen, obsCasesRead, ]; diff --git a/x-pack/test/api_integration/apis/cases/common/users.ts b/x-pack/test/api_integration/apis/cases/common/users.ts index a64b9767498fb..d3b05c5d3ddf6 100644 --- a/x-pack/test/api_integration/apis/cases/common/users.ts +++ b/x-pack/test/api_integration/apis/cases/common/users.ts @@ -31,6 +31,12 @@ import { secReadCasesAll, secReadCasesNone, secReadCasesRead, + casesV2NoReopenWithCreateComment, + obsCasesV2NoReopenWithCreateComment, + secCasesV2NoReopenWithCreateComment, + secCasesV2NoCreateCommentWithReopen, + casesV2NoCreateCommentWithReopen, + obsCasesV2NoCreateCommentWithReopen, } from './roles'; /** @@ -67,6 +73,18 @@ export const secCasesV2AllUser: User = { roles: [secCasesV2All.name], }; +export const secCasesV2NoReopenWithCreateCommentUser: User = { + username: 'sec_cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [secCasesV2NoReopenWithCreateComment.name], +}; + +export const secCasesV2NoCreateCommentWithReopenUser: User = { + username: 'sec_cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [secCasesV2NoCreateCommentWithReopen.name], +}; + export const secAllSpace1User: User = { username: 'sec_all_space1_user_api_int', password: 'password', @@ -143,6 +161,18 @@ export const casesV2AllUser: User = { roles: [casesV2All.name], }; +export const casesV2NoReopenWithCreateCommentUser: User = { + username: 'cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [casesV2NoReopenWithCreateComment.name], +}; + +export const casesV2NoCreateCommentWithReopenUser: User = { + username: 'cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [casesV2NoCreateCommentWithReopen.name], +}; + export const casesReadUser: User = { username: 'cases_read_user_api_int', password: 'password', @@ -183,6 +213,18 @@ export const obsCasesV2AllUser: User = { roles: [obsCasesV2All.name], }; +export const obsCasesV2NoReopenWithCreateCommentUser: User = { + username: 'obs_cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [obsCasesV2NoReopenWithCreateComment.name], +}; + +export const obsCasesV2NoCreateCommentWithReopenUser: User = { + username: 'obs_cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [obsCasesV2NoCreateCommentWithReopen.name], +}; + export const obsCasesReadUser: User = { username: 'obs_cases_read_user_api_int', password: 'password', @@ -211,6 +253,8 @@ export const users = [ secAllCasesNoDeleteUser, secAllUser, secCasesV2AllUser, + secCasesV2NoReopenWithCreateCommentUser, + secCasesV2NoCreateCommentWithReopenUser, secAllSpace1User, secAllCasesReadUser, secAllCasesNoneUser, @@ -223,12 +267,16 @@ export const users = [ casesNoDeleteUser, casesAllUser, casesV2AllUser, + casesV2NoReopenWithCreateCommentUser, + casesV2NoCreateCommentWithReopenUser, casesReadUser, obsCasesOnlyDeleteUser, obsCasesOnlyReadDeleteUser, obsCasesNoDeleteUser, obsCasesAllUser, obsCasesV2AllUser, + obsCasesV2NoReopenWithCreateCommentUser, + obsCasesV2NoCreateCommentWithReopenUser, obsCasesReadUser, obsSecCasesAllUser, obsSecCasesReadUser, diff --git a/x-pack/test/api_integration/apis/cases/privileges.ts b/x-pack/test/api_integration/apis/cases/privileges.ts index 53a1767f5c1a7..4e2baeeffa515 100644 --- a/x-pack/test/api_integration/apis/cases/privileges.ts +++ b/x-pack/test/api_integration/apis/cases/privileges.ts @@ -40,6 +40,12 @@ import { secReadCasesNoneUser, secReadCasesReadUser, secReadUser, + casesV2NoReopenWithCreateCommentUser, + casesV2NoCreateCommentWithReopenUser, + obsCasesV2NoReopenWithCreateCommentUser, + obsCasesV2NoCreateCommentWithReopenUser, + secCasesV2NoReopenWithCreateCommentUser, + secCasesV2NoCreateCommentWithReopenUser, } from './common/users'; import { getPostCaseRequest } from '../../../cases_api_integration/common/lib/mock'; @@ -183,6 +189,9 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, { user: casesV2AllUser, owner: CASES_APP_ID }, + { user: casesV2NoCreateCommentWithReopenUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoCreateCommentWithReopenUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoCreateCommentWithReopenUser, owner: SECURITY_SOLUTION_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can reopen a case`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -206,6 +215,35 @@ export default ({ getService }: FtrProviderContext): void => { }); } + for (const { user, owner } of [ + { user: casesV2NoReopenWithCreateCommentUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoReopenWithCreateCommentUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoReopenWithCreateCommentUser, owner: SECURITY_SOLUTION_APP_ID }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CANNOT reopen a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'closed' as CaseStatuses, + version: '2', + expectedHttpCode: 200, + auth: { user, space: null }, + }); + + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'open' as CaseStatuses, + version: '3', + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } + for (const { user, owner } of [ { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, @@ -213,6 +251,9 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, { user: casesV2AllUser, owner: CASES_APP_ID }, + { user: casesV2NoReopenWithCreateCommentUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoReopenWithCreateCommentUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoReopenWithCreateCommentUser, owner: SECURITY_SOLUTION_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can add comments`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -230,5 +271,29 @@ export default ({ getService }: FtrProviderContext): void => { }); }); } + + for (const { user, owner } of [ + { user: casesV2NoCreateCommentWithReopenUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoCreateCommentWithReopenUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoCreateCommentWithReopenUser, owner: SECURITY_SOLUTION_APP_ID }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CANNOT add comments`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + const comment: UserCommentAttachmentPayload = { + comment: 'test', + owner, + type: AttachmentType.user, + }; + await createComment({ + params: comment, + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } }); }; diff --git a/x-pack/test/api_integration/apis/content_management/dashboard_search.ts b/x-pack/test/api_integration/apis/content_management/dashboard_search.ts new file mode 100644 index 0000000000000..9a1739d508d1f --- /dev/null +++ b/x-pack/test/api_integration/apis/content_management/dashboard_search.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { sampleDashboard } from './helpers'; + +export default function ({ getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + describe('search dashboards', function () { + const createPayload = { + ...sampleDashboard, + options: { + ...sampleDashboard.options, + references: [ + { + type: 'tag', + id: 'tag1', + name: 'tag-ref-tag1', + }, + { + type: 'index-pattern', + id: 'index-pattern1', + name: 'index-pattern-ref-index-pattern1', + }, + ], + }, + }; + before(async () => { + await kibanaServer.savedObjects.clean({ + types: ['dashboard'], + }); + + await supertest + .post('/api/content_management/rpc/create') + .set('kbn-xsrf', 'true') + .send(createPayload) + .expect(200); + }); + + it('can specify references to return', async () => { + const searchPayload = { + contentTypeId: 'dashboard', + version: 3, + query: {}, + options: {}, + }; + + { + const { body } = await supertest + .post('/api/content_management/rpc/search') + .set('kbn-xsrf', 'true') + .send(searchPayload) + .expect(200); + + expect(body.result.result.hits[0].references).to.eql(createPayload.options.references); + } + + { + const { body } = await supertest + .post('/api/content_management/rpc/search') + .set('kbn-xsrf', 'true') + .send({ ...searchPayload, options: { includeReferences: ['tag'] } }) + .expect(200); + + expect(body.result.result.hits[0].references).to.eql([createPayload.options.references[0]]); + } + }); + }); +} diff --git a/x-pack/test/api_integration/apis/content_management/index.ts b/x-pack/test/api_integration/apis/content_management/index.ts index 992aafa74d27b..26541d6e7e468 100644 --- a/x-pack/test/api_integration/apis/content_management/index.ts +++ b/x-pack/test/api_integration/apis/content_management/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./created_by')); loadTestFile(require.resolve('./updated_by')); loadTestFile(require.resolve('./favorites')); + loadTestFile(require.resolve('./dashboard_search')); }); } diff --git a/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts index a066999a08b51..e1ddf399d5722 100644 --- a/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts +++ b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import moment from 'moment'; import type { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api'; @@ -14,20 +13,25 @@ import { getCommonRequestHeader } from '../../../../functional/services/ml/commo export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + let testStart: number; describe('GET notifications count', () => { + before(async () => { + testStart = Date.now(); + }); + describe('when no ML entities present', () => { it('return a default response', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications/count`) - .query({ lastCheckedAt: moment().subtract(7, 'd').valueOf() }) + .query({ lastCheckedAt: testStart }) .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); - expect(body.info).to.eql(0); - expect(body.warning).to.eql(0); - expect(body.error).to.eql(0); + expect(body.info).to.eql(0, `Expecting info count to be 0, got ${body.info}`); + expect(body.warning).to.eql(0, `Expecting warning count to be 0, got ${body.warning}`); + expect(body.error).to.eql(0, `Expecting error count to be 0, got ${body.error}`); }); }); @@ -36,10 +40,11 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.initSavedObjects(); await ml.testResources.setKibanaTimeZoneToUTC(); - const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_job'); + const jobId = `fq_job_${Date.now()}`; + const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig(jobId); await ml.api.createAnomalyDetectionJob(adJobConfig); - await ml.api.waitForJobNotificationsToIndex('fq_job'); + await ml.api.waitForJobNotificationsToIndex(jobId); }); after(async () => { @@ -50,14 +55,14 @@ export default ({ getService }: FtrProviderContext) => { it('return notifications count by level', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications/count`) - .query({ lastCheckedAt: moment().subtract(7, 'd').valueOf() }) + .query({ lastCheckedAt: testStart }) .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); - expect(body.info).to.eql(1); - expect(body.warning).to.eql(0); - expect(body.error).to.eql(0); + expect(body.info).to.eql(1, `Expecting info count to be 1, got ${body.info}`); + expect(body.warning).to.eql(0, `Expecting warning count to be 0, got ${body.warning}`); + expect(body.error).to.eql(0, `Expecting error count to be 0, got ${body.error}`); }); it('returns an error for unauthorized user', async () => { diff --git a/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts index e13f1869e6659..5e5f2c584c46f 100644 --- a/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts +++ b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts @@ -18,9 +18,11 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const ml = getService('ml'); + let testStart: number; describe('GET notifications', () => { before(async () => { + testStart = Date.now(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); await ml.api.initSavedObjects(); await ml.testResources.setKibanaTimeZoneToUTC(); @@ -45,7 +47,7 @@ export default ({ getService }: FtrProviderContext) => { it('return all notifications ', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications`) - .query({ earliest: 'now-1d', latest: 'now' }) + .query({ earliest: testStart, latest: 'now' }) .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); @@ -56,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { it('return notifications based on the query string', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications`) - .query({ earliest: 'now-1d', latest: 'now', queryString: 'job_type:anomaly_detector' }) + .query({ earliest: testStart, latest: 'now', queryString: 'job_type:anomaly_detector' }) .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); @@ -72,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { it('supports sorting asc sorting by field', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications`) - .query({ earliest: 'now-1d', latest: 'now', sortField: 'job_id', sortDirection: 'asc' }) + .query({ earliest: testStart, latest: 'now', sortField: 'job_id', sortDirection: 'asc' }) .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); @@ -83,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => { it('supports sorting desc sorting by field', async () => { const { body, status } = await supertest .get(`/internal/ml/notifications`) - .query({ earliest: 'now-1h', latest: 'now', sortField: 'job_id', sortDirection: 'desc' }) + .query({ earliest: testStart, latest: 'now', sortField: 'job_id', sortDirection: 'desc' }) .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) .set(getCommonRequestHeader('1')); ml.api.assertResponseStatusCode(200, status, body); diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts index 78f4347cd091e..a3953a87b82b2 100644 --- a/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts @@ -32,7 +32,6 @@ export default ({ getService }: FtrProviderContext) => { }); after(async () => { - await ml.api.cleanMlIndices(); await esDeleteAllIndices('user-index_dfa*'); // delete created ingest pipelines @@ -42,6 +41,7 @@ export default ({ getService }: FtrProviderContext) => { ) ); await ml.testResources.cleanMLSavedObjects(); + await ml.api.cleanMlIndices(); }); it('returns all trained models with associated pipelines including aliases', async () => { diff --git a/x-pack/test/api_integration/apis/streams/config.ts b/x-pack/test/api_integration/apis/streams/config.ts new file mode 100644 index 0000000000000..c737db9499836 --- /dev/null +++ b/x-pack/test/api_integration/apis/streams/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/api_integration/apis/streams/full_flow.ts b/x-pack/test/api_integration/apis/streams/full_flow.ts new file mode 100644 index 0000000000000..03c0cc9e0e219 --- /dev/null +++ b/x-pack/test/api_integration/apis/streams/full_flow.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + deleteStream, + enableStreams, + fetchDocument, + forkStream, + indexDocument, +} from './helpers/requests'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { waitForDocumentInIndex } from '../../../alerting_api_integration/observability/helpers/alerting_wait_for_helpers'; +import { cleanUpRootStream } from './helpers/cleanup'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esClient = getService('es'); + const retryService = getService('retry'); + const logger = getService('log'); + + describe('Basic functionality', () => { + after(async () => { + await deleteStream(supertest, 'logs.nginx'); + await cleanUpRootStream(esClient); + }); + + // Note: Each step is dependent on the previous + describe('Full flow', () => { + it('Enable streams', async () => { + await enableStreams(supertest); + }); + + it('Index a JSON document to logs, should go to logs', async () => { + const doc = { + '@timestamp': '2024-01-01T00:00:00.000Z', + message: JSON.stringify({ + 'log.level': 'info', + 'log.logger': 'nginx', + message: 'test', + }), + }; + const response = await indexDocument(esClient, 'logs', doc); + expect(response.result).to.eql('created'); + await waitForDocumentInIndex({ esClient, indexName: 'logs', retryService, logger }); + + const result = await fetchDocument(esClient, 'logs', response._id); + expect(result._index).to.match(/^\.ds\-logs-.*/); + expect(result._source).to.eql({ + '@timestamp': '2024-01-01T00:00:00.000Z', + message: 'test', + log: { level: 'info', logger: 'nginx' }, + }); + }); + + it('Fork logs to logs.nginx', async () => { + const body = { + stream: { + id: 'logs.nginx', + fields: [], + processing: [], + }, + condition: { + field: 'log.logger', + operator: 'eq', + value: 'nginx', + }, + }; + const response = await forkStream(supertest, 'logs', body); + expect(response).to.have.property('acknowledged', true); + }); + + it('Index an Nginx access log message, should goto logs.nginx', async () => { + const doc = { + '@timestamp': '2024-01-01T00:00:10.000Z', + message: JSON.stringify({ + 'log.level': 'info', + 'log.logger': 'nginx', + message: 'test', + }), + }; + const response = await indexDocument(esClient, 'logs', doc); + expect(response.result).to.eql('created'); + await waitForDocumentInIndex({ esClient, indexName: 'logs.nginx', retryService, logger }); + + const result = await fetchDocument(esClient, 'logs.nginx', response._id); + expect(result._index).to.match(/^\.ds\-logs.nginx-.*/); + expect(result._source).to.eql({ + '@timestamp': '2024-01-01T00:00:10.000Z', + message: 'test', + log: { level: 'info', logger: 'nginx' }, + }); + }); + + it('Fork logs to logs.nginx.access', async () => { + const body = { + stream: { + id: 'logs.nginx.access', + fields: [], + processing: [], + }, + condition: { field: 'log.level', operator: 'eq', value: 'info' }, + }; + const response = await forkStream(supertest, 'logs.nginx', body); + expect(response).to.have.property('acknowledged', true); + }); + + it('Index an Nginx access log message, should goto logs.nginx.access', async () => { + const doc = { + '@timestamp': '2024-01-01T00:00:20.000Z', + message: JSON.stringify({ + 'log.level': 'info', + 'log.logger': 'nginx', + message: 'test', + }), + }; + const response = await indexDocument(esClient, 'logs', doc); + expect(response.result).to.eql('created'); + await waitForDocumentInIndex({ + esClient, + indexName: 'logs.nginx.access', + retryService, + logger, + }); + + const result = await fetchDocument(esClient, 'logs.nginx.access', response._id); + expect(result._index).to.match(/^\.ds\-logs.nginx.access-.*/); + expect(result._source).to.eql({ + '@timestamp': '2024-01-01T00:00:20.000Z', + message: 'test', + log: { level: 'info', logger: 'nginx' }, + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/streams/helpers/cleanup.ts b/x-pack/test/api_integration/apis/streams/helpers/cleanup.ts new file mode 100644 index 0000000000000..f1d382031d484 --- /dev/null +++ b/x-pack/test/api_integration/apis/streams/helpers/cleanup.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; + +/** +DELETE .kibana_streams +DELETE _data_stream/logs +DELETE /_index_template/logs@stream +DELETE /_component_template/logs@stream.layer +DELETE /_ingest/pipeline/logs@json-pipeline +DELETE /_ingest/pipeline/logs@stream.processing +DELETE /_ingest/pipeline/logs@stream.reroutes +*/ + +export async function cleanUpRootStream(esClient: Client) { + await esClient.indices.delete({ index: '.kibana_streams' }); + await esClient.indices.deleteDataStream({ name: 'logs' }); + await esClient.indices.deleteIndexTemplate({ name: 'logs@stream' }); + await esClient.cluster.deleteComponentTemplate({ name: 'logs@stream.layer' }); + await esClient.ingest.deletePipeline({ id: 'logs@stream.*' }); +} diff --git a/x-pack/test/api_integration/apis/streams/helpers/requests.ts b/x-pack/test/api_integration/apis/streams/helpers/requests.ts new file mode 100644 index 0000000000000..d44644e9746b1 --- /dev/null +++ b/x-pack/test/api_integration/apis/streams/helpers/requests.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Client } from '@elastic/elasticsearch'; +import { JsonObject } from '@kbn/utility-types'; +import { Agent } from 'supertest'; +import expect from '@kbn/expect'; +import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export async function enableStreams(supertest: Agent) { + const req = supertest.post('/api/streams/_enable').set('kbn-xsrf', 'xxx'); + const response = await req.send().expect(200); + return response.body; +} + +export async function indexDocument(esClient: Client, index: string, document: JsonObject) { + const response = await esClient.index({ index, document }); + return response; +} + +export async function fetchDocument(esClient: Client, index: string, id: string) { + const query = { + ids: { values: [id] }, + }; + const response = await esClient.search({ index, query }); + expect((response.hits.total as SearchTotalHits).value).to.eql(1); + return response.hits.hits[0]; +} + +export async function forkStream(supertest: Agent, root: string, body: JsonObject) { + const req = supertest.post(`/api/streams/${root}/_fork`).set('kbn-xsrf', 'xxx'); + const response = await req.send(body).expect(200); + return response.body; +} + +export async function deleteStream(supertest: Agent, id: string) { + const req = supertest.delete(`/api/streams/${id}`).set('kbn-xsrf', 'xxx'); + const response = await req.send().expect(200); + return response.body; +} diff --git a/x-pack/test/api_integration/apis/streams/index.ts b/x-pack/test/api_integration/apis/streams/index.ts new file mode 100644 index 0000000000000..0e879fd0b9b64 --- /dev/null +++ b/x-pack/test/api_integration/apis/streams/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Streams Endpoints', () => { + loadTestFile(require.resolve('./full_flow')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts index 46ad399380550..617f9bcae89b1 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts @@ -11,7 +11,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) describe('custom_dashboards', () => { loadTestFile(require.resolve('./dependency_metrics.spec.ts')); loadTestFile(require.resolve('./metadata.spec.ts')); - loadTestFile(require.resolve('./service_dependencies.spec.ts')); loadTestFile(require.resolve('./top_dependencies.spec.ts')); loadTestFile(require.resolve('./top_operations.spec.ts')); loadTestFile(require.resolve('./top_spans.spec.ts')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts deleted file mode 100644 index 4105989e5509b..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { DependencyNode } from '@kbn/apm-plugin/common/connections'; -import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -import { generateData } from './generate_data'; - -export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const apmApiClient = getService('apmApi'); - const synthtrace = getService('synthtrace'); - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const dependencyName = 'elasticsearch'; - const serviceName = 'synth-go'; - - async function callApi() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/dependencies', - params: { - path: { serviceName }, - query: { - environment: 'production', - numBuckets: 20, - offset: '1d', - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, - }, - }); - } - - describe('Dependency for service', () => { - describe('when data is not loaded', () => { - it('handles empty state #1', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect(body.serviceDependencies).to.empty(); - }); - }); - - describe('when data is loaded', () => { - let apmSynthtraceEsClient: ApmSynthtraceEsClient; - - before(async () => { - apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); - await generateData({ apmSynthtraceEsClient, start, end }); - }); - after(() => apmSynthtraceEsClient.clean()); - - it('returns a list of dependencies for a service', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect( - body.serviceDependencies.map( - ({ location }) => (location as DependencyNode).dependencyName - ) - ).to.eql([dependencyName]); - - const currentStatsLatencyValues = - body.serviceDependencies[0].currentStats.latency.timeseries; - expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); - }); - }); - }); -} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts index fb10925b9906d..8a2a75851881c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts @@ -150,9 +150,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon it('returns log rate timeseries', async () => { const response = await getLogsRateTimeseries(); expect(response.status).to.be(200); - expect( - response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667) - ).to.be(true); + expect(response.body.currentPeriod[serviceName].every(({ y }) => y === 1)).to.be(true); }); it('handles environment filter', async () => { @@ -168,10 +166,8 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); expect(response.status).to.be(200); - expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be( - 0.18181818181818182 - ); - expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091); + expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be(2); + expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(1); }); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index bc209186c869c..74013fcc56e87 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -40,6 +40,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./suggestions')); loadTestFile(require.resolve('./throughput')); loadTestFile(require.resolve('./time_range_metadata')); + loadTestFile(require.resolve('./traces')); loadTestFile(require.resolve('./transactions')); }); } diff --git a/x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instances_detailed_statistics.spec.snap b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/__snapshots__/instances_detailed_statistics.spec.snap similarity index 71% rename from x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instances_detailed_statistics.spec.snap rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/__snapshots__/instances_detailed_statistics.spec.snap index a5444cc6a6e32..dfea3c44472a8 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instances_detailed_statistics.spec.snap +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/__snapshots__/instances_detailed_statistics.spec.snap @@ -1,69 +1,69 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM API tests service_overview/instances_detailed_statistics.spec.ts basic apm_8.0.0 Service overview instances detailed statistics when data is loaded fetching data with comparison returns the right data for current and previous periods 5`] = ` +exports[`Deployment-agnostic APM API integration tests APM service_overview Service Overview Instances detailed statistics when data is loaded fetching data with comparison returns the right data for current and previous periods 5`] = ` Object { "currentPeriod": Object { - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad": Object { + "instance-a": Object { "cpuUsage": Array [ Object { "x": 1627974300000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974360000, - "y": 0.001, + "y": 0.5, }, Object { "x": 1627974420000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974480000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974540000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974600000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974660000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974720000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974780000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974840000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974900000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974960000, - "y": 0.001, + "y": 0.5, }, Object { "x": 1627975020000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975080000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975140000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627975200000, @@ -73,11 +73,11 @@ Object { "errorRate": Array [ Object { "x": 1627974300000, - "y": 0, + "y": null, }, Object { "x": 1627974360000, - "y": null, + "y": 0, }, Object { "x": 1627974420000, @@ -93,15 +93,15 @@ Object { }, Object { "x": 1627974600000, - "y": 0.125, + "y": 0, }, Object { "x": 1627974660000, - "y": 0.6, + "y": 0, }, Object { "x": 1627974720000, - "y": 0.2, + "y": 0, }, Object { "x": 1627974780000, @@ -113,7 +113,7 @@ Object { }, Object { "x": 1627974900000, - "y": 0.0666666666666667, + "y": 0, }, Object { "x": 1627974960000, @@ -129,7 +129,7 @@ Object { }, Object { "x": 1627975140000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627975200000, @@ -139,63 +139,63 @@ Object { "latency": Array [ Object { "x": 1627974300000, - "y": 34887.8888888889, + "y": null, }, Object { "x": 1627974360000, - "y": null, + "y": 1000000, }, Object { "x": 1627974420000, - "y": 4983, + "y": 1000000, }, Object { "x": 1627974480000, - "y": 41285.4, + "y": 1000000, }, Object { "x": 1627974540000, - "y": 13820.3333333333, + "y": 1000000, }, Object { "x": 1627974600000, - "y": 13782, + "y": 1000000, }, Object { "x": 1627974660000, - "y": 13392.6, + "y": 1000000, }, Object { "x": 1627974720000, - "y": 6991, + "y": 1000000, }, Object { "x": 1627974780000, - "y": 6885.85714285714, + "y": 1000000, }, Object { "x": 1627974840000, - "y": 7935, + "y": 1000000, }, Object { "x": 1627974900000, - "y": 10828.3333333333, + "y": 1000000, }, Object { "x": 1627974960000, - "y": 6079, + "y": 1000000, }, Object { "x": 1627975020000, - "y": 5217, + "y": 1000000, }, Object { "x": 1627975080000, - "y": 8477.76923076923, + "y": 1000000, }, Object { "x": 1627975140000, - "y": 5937.18181818182, + "y": 1000000, }, Object { "x": 1627975200000, @@ -205,130 +205,130 @@ Object { "memoryUsage": Array [ Object { "x": 1627974300000, - "y": 0.786640167236328, + "y": 0.416666666666667, }, Object { "x": 1627974360000, - "y": 0.786666870117188, + "y": 0.416666666666667, }, Object { "x": 1627974420000, - "y": 0.786960601806641, + "y": 0.416666666666667, }, Object { "x": 1627974480000, - "y": 0.787132263183594, + "y": 0.416666666666667, }, Object { "x": 1627974540000, - "y": 0.787441253662109, + "y": 0.416666666666667, }, Object { "x": 1627974600000, - "y": 0.787555694580078, + "y": 0.416666666666667, }, Object { "x": 1627974660000, - "y": 0.788524627685547, + "y": 0.416666666666667, }, Object { "x": 1627974720000, - "y": 0.788822174072266, + "y": 0.416666666666667, }, Object { "x": 1627974780000, - "y": 0.789054870605469, + "y": 0.416666666666667, }, Object { "x": 1627974840000, - "y": 0.78936767578125, + "y": 0.416666666666667, }, Object { "x": 1627974900000, - "y": 0.789985656738281, + "y": 0.416666666666667, }, Object { "x": 1627974960000, - "y": 0.790130615234375, + "y": 0.416666666666667, }, Object { "x": 1627975020000, - "y": 0.790508270263672, + "y": 0.416666666666667, }, Object { "x": 1627975080000, - "y": 0.791069030761719, + "y": 0.416666666666667, }, Object { "x": 1627975140000, - "y": 0.791587829589844, + "y": 0.416666666666667, }, Object { "x": 1627975200000, "y": null, }, ], - "serviceNodeName": "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", + "serviceNodeName": "instance-a", "throughput": Array [ Object { "x": 1627974300000, - "y": 9, + "y": 0, }, Object { "x": 1627974360000, - "y": 0, + "y": 20, }, Object { "x": 1627974420000, - "y": 4, + "y": 20, }, Object { "x": 1627974480000, - "y": 5, + "y": 20, }, Object { "x": 1627974540000, - "y": 6, + "y": 20, }, Object { "x": 1627974600000, - "y": 8, + "y": 20, }, Object { "x": 1627974660000, - "y": 5, + "y": 20, }, Object { "x": 1627974720000, - "y": 5, + "y": 20, }, Object { "x": 1627974780000, - "y": 7, + "y": 20, }, Object { "x": 1627974840000, - "y": 2, + "y": 20, }, Object { "x": 1627974900000, - "y": 15, + "y": 20, }, Object { "x": 1627974960000, - "y": 3, + "y": 20, }, Object { "x": 1627975020000, - "y": 8, + "y": 20, }, Object { "x": 1627975080000, - "y": 13, + "y": 20, }, Object { "x": 1627975140000, - "y": 11, + "y": 20, }, Object { "x": 1627975200000, @@ -338,77 +338,77 @@ Object { }, }, "previousPeriod": Object { - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad": Object { + "instance-a": Object { "cpuUsage": Array [ Object { "x": 1627974300000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974360000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974420000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974480000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974540000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974600000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974660000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974720000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974780000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974840000, - "y": 0.0045, + "y": 0.5, }, Object { "x": 1627974900000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974960000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627975020000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975080000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975140000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627975200000, - "y": null, + "y": 0.5, }, ], "errorRate": Array [ Object { "x": 1627974300000, - "y": 0, + "y": null, }, Object { "x": 1627974360000, @@ -416,11 +416,11 @@ Object { }, Object { "x": 1627974420000, - "y": 0.333333333333333, + "y": 0, }, Object { "x": 1627974480000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627974540000, @@ -432,11 +432,11 @@ Object { }, Object { "x": 1627974660000, - "y": 0.166666666666667, + "y": 0, }, Object { "x": 1627974720000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627974780000, @@ -448,11 +448,11 @@ Object { }, Object { "x": 1627974900000, - "y": 0.0833333333333333, + "y": 0, }, Object { "x": 1627974960000, - "y": 0.0769230769230769, + "y": 0, }, Object { "x": 1627975020000, @@ -460,214 +460,214 @@ Object { }, Object { "x": 1627975080000, - "y": 0.1, + "y": 0, }, Object { "x": 1627975140000, - "y": 0.153846153846154, + "y": 0, }, Object { "x": 1627975200000, - "y": null, + "y": 0, }, ], "latency": Array [ Object { "x": 1627974300000, - "y": 11839, + "y": null, }, Object { "x": 1627974360000, - "y": 7407, + "y": 1000000, }, Object { "x": 1627974420000, - "y": 1925569.66666667, + "y": 1000000, }, Object { "x": 1627974480000, - "y": 9017.18181818182, + "y": 1000000, }, Object { "x": 1627974540000, - "y": 63575, + "y": 1000000, }, Object { "x": 1627974600000, - "y": 7577.66666666667, + "y": 1000000, }, Object { "x": 1627974660000, - "y": 6844.33333333333, + "y": 1000000, }, Object { "x": 1627974720000, - "y": 503471, + "y": 1000000, }, Object { "x": 1627974780000, - "y": 6247.8, + "y": 1000000, }, Object { "x": 1627974840000, - "y": 1137247, + "y": 1000000, }, Object { "x": 1627974900000, - "y": 27951.6666666667, + "y": 1000000, }, Object { "x": 1627974960000, - "y": 10248.8461538462, + "y": 1000000, }, Object { "x": 1627975020000, - "y": 13529, + "y": 1000000, }, Object { "x": 1627975080000, - "y": 6691247.8, + "y": 1000000, }, Object { "x": 1627975140000, - "y": 12098.6923076923, + "y": 1000000, }, Object { "x": 1627975200000, - "y": null, + "y": 1000000, }, ], "memoryUsage": Array [ Object { "x": 1627974300000, - "y": 0.780715942382813, + "y": 0.416666666666667, }, Object { "x": 1627974360000, - "y": 0.780921936035156, + "y": 0.416666666666667, }, Object { "x": 1627974420000, - "y": 0.781166076660156, + "y": 0.416666666666667, }, Object { "x": 1627974480000, - "y": 0.781524658203125, + "y": 0.416666666666667, }, Object { "x": 1627974540000, - "y": 0.781723022460938, + "y": 0.416666666666667, }, Object { "x": 1627974600000, - "y": 0.782463073730469, + "y": 0.416666666666667, }, Object { "x": 1627974660000, - "y": 0.782634735107422, + "y": 0.416666666666667, }, Object { "x": 1627974720000, - "y": 0.782939910888672, + "y": 0.416666666666667, }, Object { "x": 1627974780000, - "y": 0.783458709716797, + "y": 0.416666666666667, }, Object { "x": 1627974840000, - "y": 0.783935546875, + "y": 0.416666666666667, }, Object { "x": 1627974900000, - "y": 0.784690856933594, + "y": 0.416666666666667, }, Object { "x": 1627974960000, - "y": 0.785182952880859, + "y": 0.416666666666667, }, Object { "x": 1627975020000, - "y": 0.785446166992188, + "y": 0.416666666666667, }, Object { "x": 1627975080000, - "y": 0.786224365234375, + "y": 0.416666666666667, }, Object { "x": 1627975140000, - "y": 0.786415100097656, + "y": 0.416666666666667, }, Object { "x": 1627975200000, - "y": null, + "y": 0.416666666666667, }, ], - "serviceNodeName": "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", + "serviceNodeName": "instance-a", "throughput": Array [ Object { "x": 1627974300000, - "y": 4, + "y": 0, }, Object { "x": 1627974360000, - "y": 2, + "y": 20, }, Object { "x": 1627974420000, - "y": 3, + "y": 20, }, Object { "x": 1627974480000, - "y": 11, + "y": 20, }, Object { "x": 1627974540000, - "y": 4, + "y": 20, }, Object { "x": 1627974600000, - "y": 6, + "y": 20, }, Object { "x": 1627974660000, - "y": 6, + "y": 20, }, Object { "x": 1627974720000, - "y": 11, + "y": 20, }, Object { "x": 1627974780000, - "y": 10, + "y": 20, }, Object { "x": 1627974840000, - "y": 10, + "y": 20, }, Object { "x": 1627974900000, - "y": 12, + "y": 20, }, Object { "x": 1627974960000, - "y": 13, + "y": 20, }, Object { "x": 1627975020000, - "y": 8, + "y": 20, }, Object { "x": 1627975080000, - "y": 10, + "y": 20, }, Object { "x": 1627975140000, - "y": 13, + "y": 20, }, Object { "x": 1627975200000, - "y": 0, + "y": 20, }, ], }, @@ -675,130 +675,130 @@ Object { } `; -exports[`APM API tests service_overview/instances_detailed_statistics.spec.ts basic apm_8.0.0 Service overview instances detailed statistics when data is loaded fetching data without comparison returns the right data 3`] = ` +exports[`Deployment-agnostic APM API integration tests APM service_overview Service Overview Instances detailed statistics when data is loaded fetching data without comparison returns the right data 3`] = ` Object { "currentPeriod": Object { - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad": Object { + "instance-a": Object { "cpuUsage": Array [ Object { "x": 1627973400000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627973460000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973520000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627973580000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627973640000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973700000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973760000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973820000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973880000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627973940000, - "y": 0.0045, + "y": 0.5, }, Object { "x": 1627974000000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974060000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974120000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974180000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974240000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974300000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974360000, - "y": 0.001, + "y": 0.5, }, Object { "x": 1627974420000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974480000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974540000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974600000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974660000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974720000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974780000, - "y": 0.0015, + "y": 0.5, }, Object { "x": 1627974840000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627974900000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627974960000, - "y": 0.001, + "y": 0.5, }, Object { "x": 1627975020000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975080000, - "y": 0.002, + "y": 0.5, }, Object { "x": 1627975140000, - "y": 0.0025, + "y": 0.5, }, Object { "x": 1627975200000, @@ -808,7 +808,7 @@ Object { "errorRate": Array [ Object { "x": 1627973400000, - "y": 0, + "y": null, }, Object { "x": 1627973460000, @@ -816,11 +816,11 @@ Object { }, Object { "x": 1627973520000, - "y": 0.333333333333333, + "y": 0, }, Object { "x": 1627973580000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627973640000, @@ -832,11 +832,11 @@ Object { }, Object { "x": 1627973760000, - "y": 0.166666666666667, + "y": 0, }, Object { "x": 1627973820000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627973880000, @@ -848,11 +848,11 @@ Object { }, Object { "x": 1627974000000, - "y": 0.0833333333333333, + "y": 0, }, Object { "x": 1627974060000, - "y": 0.0769230769230769, + "y": 0, }, Object { "x": 1627974120000, @@ -860,11 +860,11 @@ Object { }, Object { "x": 1627974180000, - "y": 0.1, + "y": 0, }, Object { "x": 1627974240000, - "y": 0.153846153846154, + "y": 0, }, Object { "x": 1627974300000, @@ -872,7 +872,7 @@ Object { }, Object { "x": 1627974360000, - "y": null, + "y": 0, }, Object { "x": 1627974420000, @@ -888,15 +888,15 @@ Object { }, Object { "x": 1627974600000, - "y": 0.125, + "y": 0, }, Object { "x": 1627974660000, - "y": 0.6, + "y": 0, }, Object { "x": 1627974720000, - "y": 0.2, + "y": 0, }, Object { "x": 1627974780000, @@ -908,7 +908,7 @@ Object { }, Object { "x": 1627974900000, - "y": 0.0666666666666667, + "y": 0, }, Object { "x": 1627974960000, @@ -924,7 +924,7 @@ Object { }, Object { "x": 1627975140000, - "y": 0.181818181818182, + "y": 0, }, Object { "x": 1627975200000, @@ -934,123 +934,123 @@ Object { "latency": Array [ Object { "x": 1627973400000, - "y": 11839, + "y": null, }, Object { "x": 1627973460000, - "y": 7407, + "y": 1000000, }, Object { "x": 1627973520000, - "y": 1925569.66666667, + "y": 1000000, }, Object { "x": 1627973580000, - "y": 9017.18181818182, + "y": 1000000, }, Object { "x": 1627973640000, - "y": 63575, + "y": 1000000, }, Object { "x": 1627973700000, - "y": 7577.66666666667, + "y": 1000000, }, Object { "x": 1627973760000, - "y": 6844.33333333333, + "y": 1000000, }, Object { "x": 1627973820000, - "y": 503471, + "y": 1000000, }, Object { "x": 1627973880000, - "y": 6247.8, + "y": 1000000, }, Object { "x": 1627973940000, - "y": 1137247, + "y": 1000000, }, Object { "x": 1627974000000, - "y": 27951.6666666667, + "y": 1000000, }, Object { "x": 1627974060000, - "y": 10248.8461538462, + "y": 1000000, }, Object { "x": 1627974120000, - "y": 13529, + "y": 1000000, }, Object { "x": 1627974180000, - "y": 6691247.8, + "y": 1000000, }, Object { "x": 1627974240000, - "y": 12098.6923076923, + "y": 1000000, }, Object { "x": 1627974300000, - "y": 34887.8888888889, + "y": 1000000, }, Object { "x": 1627974360000, - "y": null, + "y": 1000000, }, Object { "x": 1627974420000, - "y": 4983, + "y": 1000000, }, Object { "x": 1627974480000, - "y": 41285.4, + "y": 1000000, }, Object { "x": 1627974540000, - "y": 13820.3333333333, + "y": 1000000, }, Object { "x": 1627974600000, - "y": 13782, + "y": 1000000, }, Object { "x": 1627974660000, - "y": 13392.6, + "y": 1000000, }, Object { "x": 1627974720000, - "y": 6991, + "y": 1000000, }, Object { "x": 1627974780000, - "y": 6885.85714285714, + "y": 1000000, }, Object { "x": 1627974840000, - "y": 7935, + "y": 1000000, }, Object { "x": 1627974900000, - "y": 10828.3333333333, + "y": 1000000, }, Object { "x": 1627974960000, - "y": 6079, + "y": 1000000, }, Object { "x": 1627975020000, - "y": 5217, + "y": 1000000, }, Object { "x": 1627975080000, - "y": 8477.76923076923, + "y": 1000000, }, Object { "x": 1627975140000, - "y": 5937.18181818182, + "y": 1000000, }, Object { "x": 1627975200000, @@ -1060,250 +1060,250 @@ Object { "memoryUsage": Array [ Object { "x": 1627973400000, - "y": 0.780715942382813, + "y": 0.416666666666667, }, Object { "x": 1627973460000, - "y": 0.780921936035156, + "y": 0.416666666666667, }, Object { "x": 1627973520000, - "y": 0.781166076660156, + "y": 0.416666666666667, }, Object { "x": 1627973580000, - "y": 0.781524658203125, + "y": 0.416666666666667, }, Object { "x": 1627973640000, - "y": 0.781723022460938, + "y": 0.416666666666667, }, Object { "x": 1627973700000, - "y": 0.782463073730469, + "y": 0.416666666666667, }, Object { "x": 1627973760000, - "y": 0.782634735107422, + "y": 0.416666666666667, }, Object { "x": 1627973820000, - "y": 0.782939910888672, + "y": 0.416666666666667, }, Object { "x": 1627973880000, - "y": 0.783458709716797, + "y": 0.416666666666667, }, Object { "x": 1627973940000, - "y": 0.783935546875, + "y": 0.416666666666667, }, Object { "x": 1627974000000, - "y": 0.784690856933594, + "y": 0.416666666666667, }, Object { "x": 1627974060000, - "y": 0.785182952880859, + "y": 0.416666666666667, }, Object { "x": 1627974120000, - "y": 0.785446166992188, + "y": 0.416666666666667, }, Object { "x": 1627974180000, - "y": 0.786224365234375, + "y": 0.416666666666667, }, Object { "x": 1627974240000, - "y": 0.786415100097656, + "y": 0.416666666666667, }, Object { "x": 1627974300000, - "y": 0.786640167236328, + "y": 0.416666666666667, }, Object { "x": 1627974360000, - "y": 0.786666870117188, + "y": 0.416666666666667, }, Object { "x": 1627974420000, - "y": 0.786960601806641, + "y": 0.416666666666667, }, Object { "x": 1627974480000, - "y": 0.787132263183594, + "y": 0.416666666666667, }, Object { "x": 1627974540000, - "y": 0.787441253662109, + "y": 0.416666666666667, }, Object { "x": 1627974600000, - "y": 0.787555694580078, + "y": 0.416666666666667, }, Object { "x": 1627974660000, - "y": 0.788524627685547, + "y": 0.416666666666667, }, Object { "x": 1627974720000, - "y": 0.788822174072266, + "y": 0.416666666666667, }, Object { "x": 1627974780000, - "y": 0.789054870605469, + "y": 0.416666666666667, }, Object { "x": 1627974840000, - "y": 0.78936767578125, + "y": 0.416666666666667, }, Object { "x": 1627974900000, - "y": 0.789985656738281, + "y": 0.416666666666667, }, Object { "x": 1627974960000, - "y": 0.790130615234375, + "y": 0.416666666666667, }, Object { "x": 1627975020000, - "y": 0.790508270263672, + "y": 0.416666666666667, }, Object { "x": 1627975080000, - "y": 0.791069030761719, + "y": 0.416666666666667, }, Object { "x": 1627975140000, - "y": 0.791587829589844, + "y": 0.416666666666667, }, Object { "x": 1627975200000, "y": null, }, ], - "serviceNodeName": "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", + "serviceNodeName": "instance-a", "throughput": Array [ Object { "x": 1627973400000, - "y": 4, + "y": 0, }, Object { "x": 1627973460000, - "y": 2, + "y": 20, }, Object { "x": 1627973520000, - "y": 3, + "y": 20, }, Object { "x": 1627973580000, - "y": 11, + "y": 20, }, Object { "x": 1627973640000, - "y": 4, + "y": 20, }, Object { "x": 1627973700000, - "y": 6, + "y": 20, }, Object { "x": 1627973760000, - "y": 6, + "y": 20, }, Object { "x": 1627973820000, - "y": 11, + "y": 20, }, Object { "x": 1627973880000, - "y": 10, + "y": 20, }, Object { "x": 1627973940000, - "y": 10, + "y": 20, }, Object { "x": 1627974000000, - "y": 12, + "y": 20, }, Object { "x": 1627974060000, - "y": 13, + "y": 20, }, Object { "x": 1627974120000, - "y": 8, + "y": 20, }, Object { "x": 1627974180000, - "y": 10, + "y": 20, }, Object { "x": 1627974240000, - "y": 13, + "y": 20, }, Object { "x": 1627974300000, - "y": 9, + "y": 20, }, Object { "x": 1627974360000, - "y": 0, + "y": 20, }, Object { "x": 1627974420000, - "y": 4, + "y": 20, }, Object { "x": 1627974480000, - "y": 5, + "y": 20, }, Object { "x": 1627974540000, - "y": 6, + "y": 20, }, Object { "x": 1627974600000, - "y": 8, + "y": 20, }, Object { "x": 1627974660000, - "y": 5, + "y": 20, }, Object { "x": 1627974720000, - "y": 5, + "y": 20, }, Object { "x": 1627974780000, - "y": 7, + "y": 20, }, Object { "x": 1627974840000, - "y": 2, + "y": 20, }, Object { "x": 1627974900000, - "y": 15, + "y": 20, }, Object { "x": 1627974960000, - "y": 3, + "y": 20, }, Object { "x": 1627975020000, - "y": 8, + "y": 20, }, Object { "x": 1627975080000, - "y": 13, + "y": 20, }, Object { "x": 1627975140000, - "y": 11, + "y": 20, }, Object { "x": 1627975200000, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts index cb7f81304c3b0..a579e196e7aa4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts @@ -4,301 +4,328 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import expect from '@kbn/expect'; import { last, pick } from 'lodash'; +import { DependencyNode } from '@kbn/apm-plugin/common/connections'; import type { ValuesType } from 'utility-types'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { type Node, NodeType } from '@kbn/apm-plugin/common/connections'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, } from '@kbn/apm-plugin/common/environment_filter_values'; -import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { roundNumber } from '../../utils/common'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { roundNumber } from '../../utils/common'; +import { generateDependencyData } from '../generate_data'; import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); - - const { start, end } = { - start: '2021-08-03T06:50:15.910Z', - end: '2021-08-03T07:20:15.910Z', - }; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const dependencyName = 'elasticsearch'; + const serviceName = 'synth-go'; function getName(node: Node) { return node.type === NodeType.service ? node.serviceName : node.dependencyName; } - describe('Service Overview', () => { - describe('Dependencies', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start, - end, - numBuckets: 20, - environment: ENVIRONMENT_ALL.value, - }, - }, - }); + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/dependencies', + params: { + path: { serviceName }, + query: { + environment: 'production', + numBuckets: 20, + offset: '1d', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } - expect(response.status).to.be(200); - expect(response.body.serviceDependencies).to.eql([]); - }); + describe('Dependency for service', () => { + describe('when data is not loaded', () => { + it('handles empty state #1', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.serviceDependencies).to.be.empty(); }); + }); - describe('when specific data is loaded', () => { - let response: { - status: number; - body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; - }; + describe('when specific data is loaded', () => { + let response: { + status: number; + body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; + }; + + const indices = { + metric: 'apm-dependencies-metric', + transaction: 'apm-dependencies-transaction', + span: 'apm-dependencies-span', + }; + + const startTime = new Date(start).getTime(); + const endTime = new Date(end).getTime(); + + after(async () => { + const allIndices = Object.values(indices).join(','); + const indexExists = await es.indices.exists({ index: allIndices }); + if (indexExists) { + await es.indices.delete({ + index: allIndices, + }); + } + }); - const indices = { - metric: 'apm-dependencies-metric', - transaction: 'apm-dependencies-transaction', - span: 'apm-dependencies-span', - }; + before(async () => { + await es.indices.create({ + index: indices.metric, + body: { + mappings: apmDependenciesMapping, + }, + }); - const startTime = new Date(start).getTime(); - const endTime = new Date(end).getTime(); - - after(async () => { - const allIndices = Object.values(indices).join(','); - const indexExists = await es.indices.exists({ index: allIndices }); - if (indexExists) { - await es.indices.delete({ - index: allIndices, - }); - } + await es.indices.create({ + index: indices.transaction, + body: { + mappings: apmDependenciesMapping, + }, }); - before(async () => { - await es.indices.create({ - index: indices.metric, - body: { - mappings: apmDependenciesMapping, - }, - }); + await es.indices.create({ + index: indices.span, + body: { + mappings: apmDependenciesMapping, + }, + }); - await es.indices.create({ - index: indices.transaction, - body: { - mappings: apmDependenciesMapping, + const docs = [ + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', }, - }); - - await es.indices.create({ - index: indices.span, - body: { - mappings: apmDependenciesMapping, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', }, - }); - - const docs = [ - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node:3000', - outcome: 'success', - responseTime: { - count: 2, - sum: 10, - }, - time: startTime, - to: { - service: { - name: 'opbeans-node', - }, - agentName: 'nodejs', - }, - }), - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node:3000', - outcome: 'failure', - responseTime: { - count: 1, - sum: 10, - }, - time: startTime, - }), - ...createServiceDependencyDocs({ + resource: 'opbeans-node:3000', + outcome: 'success', + responseTime: { + count: 2, + sum: 10, + }, + time: startTime, + to: { service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'postgres', - outcome: 'success', - responseTime: { - count: 1, - sum: 3, + name: 'opbeans-node', }, - time: startTime, - }), - ...createServiceDependencyDocs({ + agentName: 'nodejs', + }, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node:3000', + outcome: 'failure', + responseTime: { + count: 1, + sum: 10, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'postgres', + outcome: 'success', + responseTime: { + count: 1, + sum: 3, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node-via-proxy', + outcome: 'success', + responseTime: { + count: 1, + sum: 1, + }, + time: endTime - 1, + to: { service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node-via-proxy', - outcome: 'success', - responseTime: { - count: 1, - sum: 1, + name: 'opbeans-node', }, - time: endTime - 1, - to: { - service: { - name: 'opbeans-node', - }, - agentName: 'nodejs', - }, - }), - ]; - - const bulkActions = docs.reduce( - (prev, doc) => { - return [...prev, { index: { _index: indices[doc.processor.event] } }, doc]; + agentName: 'nodejs', }, - [] as Array< - | { - index: { - _index: string; - }; - } - | ValuesType<typeof docs> - > - ); - - await es.bulk({ - body: bulkActions, - refresh: 'wait_for', - }); + }), + ]; + + const bulkActions = docs.reduce( + (prev, doc) => { + return [...prev, { index: { _index: indices[doc.processor.event] } }, doc]; + }, + [] as Array< + | { + index: { + _index: string; + }; + } + | ValuesType<typeof docs> + > + ); + + await es.bulk({ + body: bulkActions, + refresh: 'wait_for', + }); - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start, - end, - numBuckets: 20, - environment: ENVIRONMENT_ALL.value, - }, + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, }, - }); + }, }); + }); - it('returns a 200', () => { - expect(response.status).to.be(200); - }); + it('returns a 200', () => { + expect(response.status).to.be(200); + }); - it('returns two dependencies', () => { - expect(response.body.serviceDependencies.length).to.be(2); - }); + it('returns two dependencies', () => { + expect(response.body.serviceDependencies.length).to.be(2); + }); - it('returns opbeans-node as a dependency', () => { - const opbeansNode = response.body.serviceDependencies.find( - (item) => getName(item.location) === 'opbeans-node' - ); - - expect(opbeansNode !== undefined).to.be(true); - - const values = { - latency: roundNumber(opbeansNode?.currentStats.latency.value), - throughput: roundNumber(opbeansNode?.currentStats.throughput.value), - errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value), - impact: opbeansNode?.currentStats.impact, - ...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'), - }; - - const count = 4; - const sum = 21; - const errors = 1; - - expect(values).to.eql({ - agentName: 'nodejs', - environment: ENVIRONMENT_NOT_DEFINED.value, - serviceName: 'opbeans-node', - type: 'service', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 100, - }); + it('returns opbeans-node as a dependency', () => { + const opbeansNode = response.body.serviceDependencies.find( + (item) => getName(item.location) === 'opbeans-node' + ); - const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y); - const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y); + expect(opbeansNode !== undefined).to.be(true); - expect(firstValue).to.be(roundNumber(20 / 3)); - expect(lastValue).to.be(1); + const values = { + latency: roundNumber(opbeansNode?.currentStats.latency.value), + throughput: roundNumber(opbeansNode?.currentStats.throughput.value), + errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value), + impact: opbeansNode?.currentStats.impact, + ...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'), + }; + + const count = 4; + const sum = 21; + const errors = 1; + + expect(values).to.eql({ + agentName: 'nodejs', + environment: ENVIRONMENT_NOT_DEFINED.value, + serviceName: 'opbeans-node', + type: 'service', + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), + impact: 100, }); - it('returns postgres as an external dependency', () => { - const postgres = response.body.serviceDependencies.find( - (item) => getName(item.location) === 'postgres' - ); - - expect(postgres !== undefined).to.be(true); - - const values = { - latency: roundNumber(postgres?.currentStats.latency.value), - throughput: roundNumber(postgres?.currentStats.throughput.value), - errorRate: roundNumber(postgres?.currentStats.errorRate.value), - impact: postgres?.currentStats.impact, - ...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'), - }; - - const count = 1; - const sum = 3; - const errors = 0; - - expect(values).to.eql({ - spanType: 'external', - spanSubtype: 'http', - dependencyName: 'postgres', - type: 'dependency', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 0, - }); + const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y); + const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y); + + expect(firstValue).to.be(roundNumber(20 / 3)); + expect(lastValue).to.be(1); + }); + + it('returns postgres as an external dependency', () => { + const postgres = response.body.serviceDependencies.find( + (item) => getName(item.location) === 'postgres' + ); + + expect(postgres !== undefined).to.be(true); + + const values = { + latency: roundNumber(postgres?.currentStats.latency.value), + throughput: roundNumber(postgres?.currentStats.throughput.value), + errorRate: roundNumber(postgres?.currentStats.errorRate.value), + impact: postgres?.currentStats.impact, + ...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'), + }; + + const count = 1; + const sum = 3; + const errors = 0; + + expect(values).to.eql({ + spanType: 'external', + spanSubtype: 'http', + dependencyName: 'postgres', + type: 'dependency', + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), + impact: 0, }); }); + }); - // UNSUPPORTED TEST CASES - when data is loaded - // TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743 + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateDependencyData({ apmSynthtraceEsClient, start, end }); + }); + after(() => apmSynthtraceEsClient.clean()); + + it('returns a list of dependencies for a service', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect( + body.serviceDependencies.map( + ({ location }) => (location as DependencyNode).dependencyName + ) + ).to.eql([dependencyName]); + + const currentStatsLatencyValues = + body.serviceDependencies[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); + }); }); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/generate_data.ts new file mode 100644 index 0000000000000..150a3ff00fde3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/generate_data.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 { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; + +export const dataConfig = { + rate: 20, + transaction: { + name: 'GET /api/product/list', + duration: 1000, + }, + span: { + name: 'GET apm-*/_search', + type: 'db', + subType: 'elasticsearch', + destination: 'elasticsearch', + }, +} as const; + +export async function generateServiceData({ + apmSynthtraceEsClient, + start, + end, + name, + environment, + agentName, +}: { + apmSynthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; + name: string; + environment: string; + agentName: string; +}) { + const instance = apm.service({ name, environment, agentName }).instance('instance-a'); + const { rate, transaction, span } = dataConfig; + + await apmSynthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(rate) + .generator((timestamp) => + instance + .transaction({ transactionName: transaction.name }) + .timestamp(timestamp) + .duration(transaction.duration) + .success() + .children( + instance + .span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType }) + .duration(transaction.duration) + .success() + .destination(span.destination) + .timestamp(timestamp) + ) + ), + timerange(start, end) + .interval('1m') + .rate(rate) + .generator((timestamp) => + instance + .appMetrics({ + 'system.process.cpu.total.norm.pct': 0.5, + 'system.memory.total': 120000, + 'system.process.cgroup.memory.mem.usage.bytes': 50000, + }) + .timestamp(timestamp) + ), + ]); +} + +export async function generateDependencyData({ + apmSynthtraceEsClient, + start, + end, +}: { + apmSynthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; +}) { + const instance = apm + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) + .instance('instance-a'); + const { rate, transaction, span } = dataConfig; + + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(rate) + .generator((timestamp) => + instance + .transaction({ transactionName: transaction.name }) + .timestamp(timestamp) + .duration(transaction.duration) + .success() + .children( + instance + .span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType }) + .duration(transaction.duration) + .success() + .destination(span.destination) + .timestamp(timestamp) + ) + ) + ); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts index b2596ae43c956..7fd39b650b716 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts @@ -6,12 +6,19 @@ */ import expect from '@kbn/expect'; +import moment from 'moment'; +import type { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { getServiceNodeIds } from './get_service_node_ids'; +import { generateServiceData } from './generate_data'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'opbeans-java'; @@ -20,6 +27,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon end: '2021-08-03T07:20:15.910Z', }; + interface Response { + status: number; + body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; + } + describe('Service Overview', () => { describe('Instances detailed statistics', () => { describe('when data is not loaded', () => { @@ -49,8 +61,166 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); }); - // UNSUPPORTED TEST CASES - when data is loaded - // TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743 + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateServiceData({ + apmSynthtraceEsClient, + start: new Date(start).getTime(), + end: new Date(end).getTime(), + name: serviceName, + environment: 'ENVIRONMENT_ALL', + agentName: 'java', + }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('fetching data without comparison', () => { + let response: Response; + let serviceNodeIds: string[]; + + beforeEach(async () => { + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + }); + + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + start, + end, + numBuckets: 20, + transactionType: 'request', + serviceNodeIds: JSON.stringify(serviceNodeIds), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + }); + + it('returns a service node item', () => { + expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0); + expect(Object.values(response.body.previousPeriod)).to.eql(0); + }); + + it('returns statistics for each service node', () => { + const item = response.body.currentPeriod[serviceNodeIds[0]]; + + expect(item?.cpuUsage?.some((point) => isFiniteNumber(point.y))).to.be(true); + expect(item?.memoryUsage?.some((point) => isFiniteNumber(point.y))).to.be(true); + expect(item?.errorRate?.some((point) => isFiniteNumber(point.y))).to.be(true); + expect(item?.throughput?.some((point) => isFiniteNumber(point.y))).to.be(true); + expect(item?.latency?.some((point) => isFiniteNumber(point.y))).to.be(true); + }); + + it('returns the right data', () => { + expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`); + + expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(` + Array [ + "instance-a", + ] + `); + + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('fetching data with comparison', () => { + let response: Response; + let serviceNodeIds: string[]; + + beforeEach(async () => { + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + }); + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + numBuckets: 20, + transactionType: 'request', + serviceNodeIds: JSON.stringify(serviceNodeIds), + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + offset: '15m', + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + }); + + it('returns a service node item for current and previous periods', () => { + expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0); + expect(Object.values(response.body.previousPeriod).length).to.be.greaterThan(0); + }); + + it('returns statistics for current and previous periods', () => { + const currentPeriodItem = response.body.currentPeriod[serviceNodeIds[0]]; + + function hasValidYCoordinate(point: Coordinate) { + return isFiniteNumber(point.y); + } + + expect(currentPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true); + expect(currentPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true); + expect(currentPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true); + expect(currentPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true); + expect(currentPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true); + + const previousPeriodItem = response.body.previousPeriod[serviceNodeIds[0]]; + + expect(previousPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true); + expect(previousPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true); + expect(previousPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true); + expect(previousPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true); + expect(previousPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true); + }); + + it('returns the right data for current and previous periods', () => { + expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`); + expectSnapshot(Object.values(response.body.previousPeriod).length).toMatchInline(`1`); + + expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(` + Array [ + "instance-a", + ] + `); + expectSnapshot(Object.keys(response.body.previousPeriod)).toMatchInline(` + Array [ + "instance-a", + ] + `); + + expectSnapshot(response.body).toMatch(); + }); + + it('matches x-axis on current period and previous period', () => { + const currentLatencyItems = response.body.currentPeriod[serviceNodeIds[0]]?.latency; + const previousLatencyItems = response.body.previousPeriod[serviceNodeIds[0]]?.latency; + + expect(currentLatencyItems?.map(({ x }) => x)).to.be.eql( + previousLatencyItems?.map(({ x }) => x) + ); + }); + }); + }); }); }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/__snapshots__/top_traces.spec.snap similarity index 53% rename from x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/__snapshots__/top_traces.spec.snap index 77aba0cdd3bf6..b2a69a7a098d5 100644 --- a/x-pack/test/apm_api_integration/tests/traces/__snapshots__/top_traces.spec.snap +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/__snapshots__/top_traces.spec.snap @@ -1,76 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM API tests traces/top_traces.spec.ts basic apm_8.0.0 Top traces when data is loaded returns the correct buckets 1`] = ` +exports[`Deployment-agnostic APM API integration tests APM Traces Top traces when data is loaded returns the correct buckets 1`] = ` Array [ - Object { - "agentName": "java", - "averageResponseTime": 1639, - "impact": 0, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doPost", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doPost", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 3279, - "impact": 0.00144735571024101, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "POST /api/orders", - }, - "serviceName": "opbeans-node", - "transactionName": "POST /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 6175, - "impact": 0.00400317408637392, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 3495, - "impact": 0.00472243927164613, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "POST Orders/Post", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "POST Orders/Post", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "agentName": "python", - "averageResponseTime": 7039, - "impact": 0.00476568343615943, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, Object { "agentName": "ruby", - "averageResponseTime": 6303, - "impact": 0.00967875004525193, + "averageResponseTime": 5664, + "impact": 0, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::OrdersController#create", @@ -78,12 +13,12 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::OrdersController#create", "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "java", - "averageResponseTime": 7209.66666666667, - "impact": 0.0176418540534865, + "averageResponseTime": 6031, + "impact": 0.000810693162092553, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#products", @@ -91,12 +26,12 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#products", "transactionType": "request", - "transactionsPerMinute": 0.1, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "java", - "averageResponseTime": 4511, - "impact": 0.0224401912465233, + "averageResponseTime": 4506.5, + "impact": 0.00739785122574376, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#orders", @@ -104,103 +39,51 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#orders", "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "agentName": "python", - "averageResponseTime": 7607, - "impact": 0.0254072704525173, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.order", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.order", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 10143, - "impact": 0.025408152986487, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "agentName": "ruby", - "averageResponseTime": 6105.66666666667, - "impact": 0.0308842762682221, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::TypesController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::TypesController#index", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "agentName": "java", - "averageResponseTime": 6116.33333333333, - "impact": 0.0309407584422802, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customerWhoBought", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customerWhoBought", - "transactionType": "request", - "transactionsPerMinute": 0.2, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "java", - "averageResponseTime": 12543, - "impact": 0.0317623975680329, + "averageResponseTime": 9534, + "impact": 0.00854872625966807, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#customers", + "transaction.name": "APIRestController#product", }, "serviceName": "opbeans-java", - "transactionName": "APIRestController#customers", + "transactionName": "APIRestController#product", "transactionType": "request", - "transactionsPerMinute": 0.1, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "nodejs", - "averageResponseTime": 5551, - "impact": 0.0328461492827744, + "averageResponseTime": 10075, + "impact": 0.00974378075746663, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/orders/:id", + "transaction.name": "POST /api", }, "serviceName": "opbeans-node", - "transactionName": "GET /api/orders/:id", + "transactionName": "POST /api", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "java", - "averageResponseTime": 13183, - "impact": 0.0334568627897785, + "averageResponseTime": 4839.33333333333, + "impact": 0.0195582486571321, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#stats", + "transaction.name": "APIRestController#topProducts", }, "serviceName": "opbeans-java", - "transactionName": "APIRestController#stats", + "transactionName": "APIRestController#topProducts", "transactionType": "request", - "transactionsPerMinute": 0.1, + "transactionsPerMinute": 0.05, }, Object { "agentName": "go", - "averageResponseTime": 8050.2, - "impact": 0.0340764016364792, + "averageResponseTime": 7530.5, + "impact": 0.020757721101318, "key": Object { "service.name": "opbeans-go", "transaction.name": "POST /api/orders", @@ -208,51 +91,25 @@ Array [ "serviceName": "opbeans-go", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "agentName": "ruby", - "averageResponseTime": 10079, - "impact": 0.0341337663445071, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#show", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 8463, - "impact": 0.0358979517498557, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "ruby", - "averageResponseTime": 10799, - "impact": 0.0366754641771254, + "averageResponseTime": 7582.5, + "impact": 0.0209874543134642, "key": Object { "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#show", + "transaction.name": "Api::CustomersController#show", }, "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#show", + "transactionName": "Api::CustomersController#show", "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "ruby", - "averageResponseTime": 7428.33333333333, - "impact": 0.0378880658514371, + "averageResponseTime": 8001, + "impact": 0.0228363648766017, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::TypesController#show", @@ -260,12 +117,12 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::TypesController#show", "transactionType": "request", - "transactionsPerMinute": 0.2, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "java", - "averageResponseTime": 3105.13333333333, - "impact": 0.039659311528543, + "averageResponseTime": 3259.4, + "impact": 0.0234880119687469, "key": Object { "service.name": "opbeans-java", "transaction.name": "ResourceHttpRequestHandler", @@ -273,454 +130,402 @@ Array [ "serviceName": "opbeans-java", "transactionName": "ResourceHttpRequestHandler", "transactionType": "request", - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.0833333333333333, }, Object { - "agentName": "java", - "averageResponseTime": 6883.57142857143, - "impact": 0.0410784261517549, + "agentName": "nodejs", + "averageResponseTime": 5675, + "impact": 0.0250961444537697, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order", + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/top", }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#order", + "serviceName": "opbeans-node", + "transactionName": "GET /api/products/top", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.05, }, Object { - "agentName": "dotnet", - "averageResponseTime": 3505, - "impact": 0.0480460318422139, + "agentName": "nodejs", + "averageResponseTime": 8537, + "impact": 0.0252043841402617, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Products/Get", + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Products/Get", + "serviceName": "opbeans-node", + "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 0.533333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "java", - "averageResponseTime": 5621.4, - "impact": 0.0481642913941483, + "averageResponseTime": 6006.33333333333, + "impact": 0.0272918638083201, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#topProducts", + "transaction.name": "APIRestController#customerWhoBought", }, "serviceName": "opbeans-java", - "transactionName": "APIRestController#topProducts", - "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 8428.71428571429, - "impact": 0.0506239135675883, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders", + "transactionName": "APIRestController#customerWhoBought", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.05, }, Object { "agentName": "nodejs", - "averageResponseTime": 8520.14285714286, - "impact": 0.0511887353081702, + "averageResponseTime": 18064, + "impact": 0.0273912676020372, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/customers/:id", + "transaction.name": "GET /api/products", }, "serviceName": "opbeans-node", - "transactionName": "GET /api/customers/:id", + "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "nodejs", - "averageResponseTime": 6683.44444444444, - "impact": 0.0516388276326964, + "averageResponseTime": 19217, + "impact": 0.0299382136943879, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/products/top", + "transaction.name": "GET /api/customers", }, "serviceName": "opbeans-node", - "transactionName": "GET /api/products/top", - "transactionType": "request", - "transactionsPerMinute": 0.3, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 3482.78947368421, - "impact": 0.0569534471979838, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Types/Get", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Types/Get", + "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.633333333333333, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "python", - "averageResponseTime": 16703, - "impact": 0.057517386404596, + "averageResponseTime": 22023, + "impact": 0.0361365924759457, "key": Object { "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_type", + "transaction.name": "GET opbeans.views.customer", }, "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_type", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 4943, - "impact": 0.0596266425920813, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Products/Get {id}", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Products/Get {id}", - "transactionType": "request", - "transactionsPerMinute": 0.466666666666667, - }, - Object { - "agentName": "nodejs", - "averageResponseTime": 7892.33333333333, - "impact": 0.0612407972225879, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types/:id", + "transactionName": "GET opbeans.views.customer", "transactionType": "request", - "transactionsPerMinute": 0.3, + "transactionsPerMinute": 0.0166666666666667, }, Object { - "agentName": "dotnet", - "averageResponseTime": 6346.42857142857, - "impact": 0.0769666700279444, + "agentName": "ruby", + "averageResponseTime": 12106, + "impact": 0.0409720347969828, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Orders/Get {id}", + "service.name": "opbeans-ruby", + "transaction.name": "Api::ProductsController#index", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Orders/Get {id}", + "serviceName": "opbeans-ruby", + "transactionName": "Api::ProductsController#index", "transactionType": "request", - "transactionsPerMinute": 0.466666666666667, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "agentName": "go", - "averageResponseTime": 7052.84615384615, - "impact": 0.0794704188998674, + "agentName": "ruby", + "averageResponseTime": 12331.5, + "impact": 0.0419682817073472, "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products", + "service.name": "opbeans-ruby", + "transaction.name": "Api::TypesController#index", }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products", + "serviceName": "opbeans-ruby", + "transactionName": "Api::TypesController#index", "transactionType": "request", - "transactionsPerMinute": 0.433333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "java", - "averageResponseTime": 10484.3333333333, - "impact": 0.0818285496667966, + "averageResponseTime": 12897.5, + "impact": 0.0444688393626299, "key": Object { "service.name": "opbeans-java", - "transaction.name": "APIRestController#product", + "transaction.name": "APIRestController#customers", }, "serviceName": "opbeans-java", - "transactionName": "APIRestController#product", + "transactionName": "APIRestController#customers", "transactionType": "request", - "transactionsPerMinute": 0.3, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "agentName": "nodejs", - "averageResponseTime": 23711, - "impact": 0.0822565786420813, + "agentName": "java", + "averageResponseTime": 6831.75, + "impact": 0.0478529862953978, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/stats", + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customer", }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/stats", + "serviceName": "opbeans-java", + "transactionName": "APIRestController#customer", "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "agentName": "dotnet", - "averageResponseTime": 4491.36363636364, - "impact": 0.0857567083657495, + "agentName": "ruby", + "averageResponseTime": 29231, + "impact": 0.0520588712562267, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Types/Get {id}", + "service.name": "opbeans-ruby", + "transaction.name": "Api::ProductsController#show", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Types/Get {id}", + "serviceName": "opbeans-ruby", + "transactionName": "Api::ProductsController#show", "transactionType": "request", - "transactionsPerMinute": 0.733333333333333, + "transactionsPerMinute": 0.0166666666666667, }, Object { "agentName": "python", - "averageResponseTime": 20715.8, - "impact": 0.089965512867054, + "averageResponseTime": 16222.5, + "impact": 0.0591585111008193, "key": Object { "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.stats", + "transaction.name": "GET opbeans.views.orders", }, "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.stats", + "transactionName": "GET opbeans.views.orders", "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "nodejs", - "averageResponseTime": 9036.33333333333, - "impact": 0.0942519803576885, + "averageResponseTime": 17629, + "impact": 0.065372352694733, "key": Object { "service.name": "opbeans-node", - "transaction.name": "GET /api/products", + "transaction.name": "GET /api/stats", }, "serviceName": "opbeans-node", - "transactionName": "GET /api/products", + "transactionName": "GET /api/stats", "transactionType": "request", - "transactionsPerMinute": 0.4, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "agentName": "java", - "averageResponseTime": 7504.06666666667, - "impact": 0.0978924329825326, + "agentName": "ruby", + "averageResponseTime": 9031.5, + "impact": 0.0672897414268756, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customer", + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#show", }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customer", + "serviceName": "opbeans-ruby", + "transactionName": "Api::OrdersController#show", "transactionType": "request", - "transactionsPerMinute": 0.5, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "agentName": "go", - "averageResponseTime": 4250.55555555556, - "impact": 0.0998375378516613, + "agentName": "python", + "averageResponseTime": 38278, + "impact": 0.0720434517397453, "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/types/:id", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.products", }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/types/:id", + "serviceName": "opbeans-python", + "transactionName": "GET opbeans.views.products", "transactionType": "request", - "transactionsPerMinute": 0.9, + "transactionsPerMinute": 0.0166666666666667, }, Object { - "agentName": "nodejs", - "averageResponseTime": 21343, - "impact": 0.11156906191034, + "agentName": "python", + "averageResponseTime": 19176.5, + "impact": 0.0722091247292738, "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product", }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers", + "serviceName": "opbeans-python", + "transactionName": "GET opbeans.views.product", "transactionType": "request", - "transactionsPerMinute": 0.2, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "ruby", - "averageResponseTime": 16655, - "impact": 0.116142352941114, + "averageResponseTime": 21443, + "impact": 0.0822224002163734, "key": Object { "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#top", + "transaction.name": "Api::OrdersController#index", }, "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#top", + "transactionName": "Api::OrdersController#index", "transactionType": "request", - "transactionsPerMinute": 0.266666666666667, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "go", - "averageResponseTime": 5749, - "impact": 0.12032203382142, + "averageResponseTime": 7299.66666666667, + "impact": 0.0842369837690393, "key": Object { "service.name": "opbeans-go", - "transaction.name": "GET /api/types", + "transaction.name": "GET /api/orders", }, "serviceName": "opbeans-go", - "transactionName": "GET /api/types", + "transactionName": "GET /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.8, + "transactionsPerMinute": 0.1, }, Object { "agentName": "ruby", - "averageResponseTime": 9951, - "impact": 0.121502864272824, + "averageResponseTime": 11738, + "impact": 0.0912040852220091, "key": Object { "service.name": "opbeans-ruby", - "transaction.name": "Api::StatsController#index", + "transaction.name": "Api::ProductsController#top", }, "serviceName": "opbeans-ruby", - "transactionName": "Api::StatsController#index", + "transactionName": "Api::ProductsController#top", "transactionType": "request", - "transactionsPerMinute": 0.466666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "agentName": "go", - "averageResponseTime": 14040.6, - "impact": 0.122466591367692, + "agentName": "nodejs", + "averageResponseTime": 9640.8, + "impact": 0.0939697196605374, "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/customers", + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders/:id", }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/customers", + "serviceName": "opbeans-node", + "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, + "transactionsPerMinute": 0.0833333333333333, }, Object { "agentName": "ruby", - "averageResponseTime": 20963.5714285714, - "impact": 0.128060974201361, + "averageResponseTime": 8349.33333333333, + "impact": 0.0981490969430418, "key": Object { "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#index", + "transaction.name": "Api::StatsController#index", }, "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#index", + "transactionName": "Api::StatsController#index", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.1, }, Object { - "agentName": "python", - "averageResponseTime": 22874.4285714286, - "impact": 0.139865748579522, + "agentName": "nodejs", + "averageResponseTime": 9213.16666666667, + "impact": 0.109598205006055, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customer", + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customer", + "serviceName": "opbeans-node", + "transactionName": "GET /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.1, }, Object { - "agentName": "python", - "averageResponseTime": 32203.8, - "impact": 0.140658264084276, + "agentName": "nodejs", + "averageResponseTime": 14031.25, + "impact": 0.111466996327935, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customers", + "service.name": "opbeans-node", + "transaction.name": "GET /api/types/:id", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customers", + "serviceName": "opbeans-node", + "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "agentName": "go", - "averageResponseTime": 4482.11111111111, - "impact": 0.140955678032051, + "agentName": "nodejs", + "averageResponseTime": 8779.85714285714, + "impact": 0.123249659343199, "key": Object { - "service.name": "opbeans-go", + "service.name": "opbeans-node", "transaction.name": "GET /api/customers/:id", }, - "serviceName": "opbeans-go", + "serviceName": "opbeans-node", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 1.2, + "transactionsPerMinute": 0.116666666666667, }, Object { - "agentName": "ruby", - "averageResponseTime": 12582.3846153846, - "impact": 0.142910490774846, + "agentName": "go", + "averageResponseTime": 6746.3, + "impact": 0.13651233439825, "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#index", + "service.name": "opbeans-go", + "transaction.name": "GET /api/orders/:id", }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#index", + "serviceName": "opbeans-go", + "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.433333333333333, + "transactionsPerMinute": 0.166666666666667, }, Object { - "agentName": "ruby", - "averageResponseTime": 10009.9473684211, - "impact": 0.166401779979233, + "agentName": "go", + "averageResponseTime": 7179.9, + "impact": 0.146090442166188, "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::CustomersController#show", + "service.name": "opbeans-go", + "transaction.name": "GET /api/types/:id", }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::CustomersController#show", + "serviceName": "opbeans-go", + "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.633333333333333, + "transactionsPerMinute": 0.166666666666667, }, Object { - "agentName": "python", - "averageResponseTime": 27825.2857142857, - "impact": 0.170450845832029, + "agentName": "nodejs", + "averageResponseTime": 8171.11111111111, + "impact": 0.149936264496442, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_types", + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id/customers", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_types", + "serviceName": "opbeans-node", + "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, + "transactionsPerMinute": 0.15, }, Object { - "agentName": "python", - "averageResponseTime": 20562.2, - "impact": 0.180021926732983, + "agentName": "go", + "averageResponseTime": 9247.625, + "impact": 0.150910421674869, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.orders", + "service.name": "opbeans-go", + "transaction.name": "GET /api/customers", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.orders", + "serviceName": "opbeans-go", + "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "agentName": "dotnet", - "averageResponseTime": 7106.76470588235, - "impact": 0.21180020991247, + "agentName": "python", + "averageResponseTime": 19205, + "impact": 0.157181696571819, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Customers/Get {id}", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.product_type", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Customers/Get {id}", + "serviceName": "opbeans-python", + "transactionName": "GET opbeans.views.product_type", "transactionType": "request", - "transactionsPerMinute": 1.13333333333333, + "transactionsPerMinute": 0.0666666666666667, }, Object { "agentName": "go", - "averageResponseTime": 8612.51724137931, - "impact": 0.218977858687708, + "averageResponseTime": 10272.25, + "impact": 0.169017374943732, "key": Object { "service.name": "opbeans-go", - "transaction.name": "GET /api/orders/:id", + "transaction.name": "GET /api/types", }, "serviceName": "opbeans-go", - "transactionName": "GET /api/orders/:id", + "transactionName": "GET /api/types", "transactionType": "request", - "transactionsPerMinute": 0.966666666666667, + "transactionsPerMinute": 0.133333333333333, }, Object { "agentName": "ruby", - "averageResponseTime": 11295, - "impact": 0.277663720068132, + "averageResponseTime": 10371, + "impact": 0.170762463766765, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::CustomersController#index", @@ -728,77 +533,64 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::CustomersController#index", "transactionType": "request", - "transactionsPerMinute": 0.933333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "agentName": "python", - "averageResponseTime": 65035.8, - "impact": 0.285535040543522, + "agentName": "java", + "averageResponseTime": 28415.3333333333, + "impact": 0.175794504702042, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_customers", + "service.name": "opbeans-java", + "transaction.name": "DispatcherServlet#doGet", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_customers", + "serviceName": "opbeans-java", + "transactionName": "DispatcherServlet#doGet", "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, + "transactionsPerMinute": 0.05, }, Object { "agentName": "go", - "averageResponseTime": 30999.4705882353, - "impact": 0.463640986028375, + "averageResponseTime": 6374, + "impact": 0.184608307744956, "key": Object { "service.name": "opbeans-go", - "transaction.name": "GET /api/stats", + "transaction.name": "GET /api/products", }, "serviceName": "opbeans-go", - "transactionName": "GET /api/stats", + "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.566666666666667, + "transactionsPerMinute": 0.233333333333333, }, Object { "agentName": "go", - "averageResponseTime": 20197.4, - "impact": 0.622424732781511, + "averageResponseTime": 6257.46666666667, + "impact": 0.194827017739071, "key": Object { "service.name": "opbeans-go", - "transaction.name": "GET /api/products/:id/customers", + "transaction.name": "GET /api/customers/:id", }, "serviceName": "opbeans-go", - "transactionName": "GET /api/products/:id/customers", + "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 1.16666666666667, + "transactionsPerMinute": 0.25, }, Object { "agentName": "python", - "averageResponseTime": 64681.6666666667, - "impact": 0.68355874339377, + "averageResponseTime": 111027.5, + "impact": 0.47800191836068, "key": Object { "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.top_products", + "transaction.name": "GET opbeans.views.stats", }, "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.top_products", - "transactionType": "request", - "transactionsPerMinute": 0.4, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 41416.1428571429, - "impact": 0.766127739061111, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Customers/Get", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Customers/Get", + "transactionName": "GET opbeans.views.stats", "transactionType": "request", - "transactionsPerMinute": 0.7, + "transactionsPerMinute": 0.0333333333333333, }, Object { "agentName": "go", - "averageResponseTime": 19429, - "impact": 0.821597646656097, + "averageResponseTime": 19844.9333333333, + "impact": 0.645042262296039, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products/:id", @@ -806,38 +598,38 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 1.6, + "transactionsPerMinute": 0.25, }, Object { - "agentName": "dotnet", - "averageResponseTime": 62390.652173913, - "impact": 1.26497653527507, + "agentName": "python", + "averageResponseTime": 153199, + "impact": 0.664313344437989, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Products/Customerwhobought {id}", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.top_products", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Products/Customerwhobought {id}", + "serviceName": "opbeans-python", + "transactionName": "GET opbeans.views.top_products", "transactionType": "request", - "transactionsPerMinute": 0.766666666666667, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "agentName": "python", - "averageResponseTime": 33266.2, - "impact": 1.76006661931225, + "agentName": "go", + "averageResponseTime": 22825, + "impact": 0.895045012467666, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_orders", + "service.name": "opbeans-go", + "transaction.name": "GET /api/stats", }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_orders", - "transactionType": "celery", - "transactionsPerMinute": 2, + "serviceName": "opbeans-go", + "transactionName": "GET /api/stats", + "transactionType": "request", + "transactionsPerMinute": 0.3, }, Object { "agentName": "nodejs", - "averageResponseTime": 38491.4444444444, - "impact": 1.83293391905112, + "averageResponseTime": 28576.8666666667, + "impact": 0.934371362235332, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api", @@ -845,90 +637,77 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api", "transactionType": "request", - "transactionsPerMinute": 1.8, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 118488.6, - "impact": 2.08995781717084, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Stats/Get", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Stats/Get", - "transactionType": "request", - "transactionsPerMinute": 0.666666666666667, + "transactionsPerMinute": 0.25, }, Object { - "agentName": "dotnet", - "averageResponseTime": 250440.142857143, - "impact": 4.64001412901584, + "agentName": "go", + "averageResponseTime": 75613.8461538462, + "impact": 2.1588648457865, "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Products/Top", + "service.name": "opbeans-go", + "transaction.name": "GET /api/products/:id/customers", }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Products/Top", + "serviceName": "opbeans-go", + "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.7, + "transactionsPerMinute": 0.216666666666667, }, Object { - "agentName": "java", - "averageResponseTime": 312096.523809524, - "impact": 5.782704992387, + "agentName": "python", + "averageResponseTime": 500211, + "impact": 2.19739375623124, "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doGet", + "service.name": "opbeans-python", + "transaction.name": "GET opbeans.views.customers", }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doGet", + "serviceName": "opbeans-python", + "transactionName": "GET opbeans.views.customers", "transactionType": "request", - "transactionsPerMinute": 0.7, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "agentName": "ruby", - "averageResponseTime": 91519.7032967033, - "impact": 7.34855500859826, + "agentName": "rum-js", + "averageResponseTime": 631900, + "impact": 13.9459899869012, "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Rack", + "service.name": "opbeans-rum", + "transaction.name": "/customers", }, - "serviceName": "opbeans-ruby", - "transactionName": "Rack", - "transactionType": "request", - "transactionsPerMinute": 3.03333333333333, + "serviceName": "opbeans-rum", + "transactionName": "/customers", + "transactionType": "page-load", + "transactionsPerMinute": 0.166666666666667, }, Object { "agentName": "rum-js", - "averageResponseTime": 648269.769230769, - "impact": 7.43611473386403, + "averageResponseTime": 1163466.66666667, + "impact": 38.5384885525045, "key": Object { "service.name": "opbeans-rum", - "transaction.name": "/customers", + "transaction.name": "/products", }, "serviceName": "opbeans-rum", - "transactionName": "/customers", + "transactionName": "/products", "transactionType": "page-load", - "transactionsPerMinute": 0.433333333333333, + "transactionsPerMinute": 0.25, }, Object { - "agentName": "python", - "averageResponseTime": 1398919.72727273, - "impact": 13.5790895084132, + "agentName": "ruby", + "averageResponseTime": 404792.666666667, + "impact": 40.2254151113471, "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.products", + "service.name": "opbeans-ruby", + "transaction.name": "Rack", }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.products", + "serviceName": "opbeans-ruby", + "transactionName": "Rack", "transactionType": "request", - "transactionsPerMinute": 0.366666666666667, + "transactionsPerMinute": 0.75, }, Object { "agentName": "rum-js", - "averageResponseTime": 1199907.57142857, - "impact": 14.8239822181408, + "averageResponseTime": 1756666.66666667, + "impact": 46.5526432992941, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/orders", @@ -936,38 +715,12 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/orders", "transactionType": "page-load", - "transactionsPerMinute": 0.466666666666667, - }, - Object { - "agentName": "rum-js", - "averageResponseTime": 955876.052631579, - "impact": 16.026822184214, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/products", - }, - "serviceName": "opbeans-rum", - "transactionName": "/products", - "transactionType": "page-load", - "transactionsPerMinute": 0.633333333333333, - }, - Object { - "agentName": "go", - "averageResponseTime": 965009.526315789, - "impact": 16.1799735991728, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.633333333333333, + "transactionsPerMinute": 0.2, }, Object { "agentName": "rum-js", - "averageResponseTime": 1213675.30769231, - "impact": 27.8474053933734, + "averageResponseTime": 1008291.66666667, + "impact": 53.4424306904839, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/dashboard", @@ -975,12 +728,12 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/dashboard", "transactionType": "page-load", - "transactionsPerMinute": 0.866666666666667, + "transactionsPerMinute": 0.4, }, Object { "agentName": "nodejs", - "averageResponseTime": 924019.363636364, - "impact": 35.8796065162284, + "averageResponseTime": 987612.65, + "impact": 87.2518831606925, "key": Object { "service.name": "opbeans-node", "transaction.name": "Update shipping status", @@ -988,72 +741,33 @@ Array [ "serviceName": "opbeans-node", "transactionName": "Update shipping status", "transactionType": "Worker", - "transactionsPerMinute": 1.46666666666667, + "transactionsPerMinute": 0.666666666666667, }, Object { "agentName": "nodejs", - "averageResponseTime": 1060469.15384615, - "impact": 36.498655556576, + "averageResponseTime": 947544.214285714, + "impact": 87.8976786828476, "key": Object { "service.name": "opbeans-node", - "transaction.name": "Process payment", + "transaction.name": "Process completed order", }, "serviceName": "opbeans-node", - "transactionName": "Process payment", + "transactionName": "Process completed order", "transactionType": "Worker", - "transactionsPerMinute": 1.3, - }, - Object { - "agentName": "python", - "averageResponseTime": 118686.822222222, - "impact": 37.7068083771466, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.update_stats", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.update_stats", - "transactionType": "celery", - "transactionsPerMinute": 12, + "transactionsPerMinute": 0.7, }, Object { "agentName": "nodejs", - "averageResponseTime": 1039228.27659574, - "impact": 43.1048035741496, + "averageResponseTime": 1077989.66666667, + "impact": 100, "key": Object { "service.name": "opbeans-node", - "transaction.name": "Process completed order", + "transaction.name": "Process payment", }, "serviceName": "opbeans-node", - "transactionName": "Process completed order", + "transactionName": "Process payment", "transactionType": "Worker", - "transactionsPerMinute": 1.56666666666667, - }, - Object { - "agentName": "python", - "averageResponseTime": 1949922.55555556, - "impact": 61.9499776921889, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_customers", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_customers", - "transactionType": "celery", - "transactionsPerMinute": 1.2, - }, - Object { - "agentName": "dotnet", - "averageResponseTime": 5963775, - "impact": 100, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Orders/Get", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Orders/Get", - "transactionType": "request", - "transactionsPerMinute": 0.633333333333333, + "transactionsPerMinute": 0.7, }, ] `; diff --git a/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/critical_path.spec.ts similarity index 79% rename from x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/critical_path.spec.ts index 6d55c55ba6dbf..1f1d28215307c 100644 --- a/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/critical_path.spec.ts @@ -10,13 +10,13 @@ import expect from '@kbn/expect'; import { Assign } from '@kbn/utility-types'; import { compact, invert, sortBy, uniq } from 'lodash'; import { Readable } from 'stream'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { SupertestReturnType } from '../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -33,79 +33,86 @@ export default function ApiTest({ getService }: FtrProviderContext) { children: FormattedNode[]; } - // format tree in somewhat concise format for easier testing - function formatTree(nodes: HydratedNode[]): FormattedNode[] { - return sortBy( - nodes.map((node) => { - const name = - node.metadata?.['processor.event'] === 'transaction' - ? node.metadata['transaction.name'] - : node.metadata?.['span.name'] || 'root'; - return { name, value: node.countExclusive, children: formatTree(node.children) }; - }), - (node) => node.name - ); - } - - async function fetchAndBuildCriticalPathTree( - options: { fn: () => SynthtraceGenerator<ApmFields> } & ( - | { serviceName: string; transactionName: string } - | {} - ) - ) { - const { fn } = options; - - const generator = fn(); - - const unserialized = Array.from(generator); - - const serialized = unserialized.flatMap((event) => event.serialize()); - - const traceIds = compact(uniq(serialized.map((event) => event['trace.id']))); - - await apmSynthtraceEsClient.index(Readable.from(unserialized)); - - return apmApiClient - .readUser({ - endpoint: 'POST /internal/apm/traces/aggregated_critical_path', - params: { - body: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - traceIds, - serviceName: 'serviceName' in options ? options.serviceName : null, - transactionName: 'transactionName' in options ? options.transactionName : null, + describe('Aggregated critical path', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + // format tree in somewhat concise format for easier testing + function formatTree(nodes: HydratedNode[]): FormattedNode[] { + return sortBy( + nodes.map((node) => { + const name = + node.metadata?.['processor.event'] === 'transaction' + ? node.metadata['transaction.name'] + : node.metadata?.['span.name'] || 'root'; + return { name, value: node.countExclusive, children: formatTree(node.children) }; + }), + (node) => node.name + ); + } + + async function fetchAndBuildCriticalPathTree( + options: { fn: () => SynthtraceGenerator<ApmFields> } & ( + | { serviceName: string; transactionName: string } + | {} + ) + ) { + const { fn } = options; + + const generator = fn(); + + const unserialized = Array.from(generator); + + const serialized = unserialized.flatMap((event) => event.serialize()); + + const traceIds = compact(uniq(serialized.map((event) => event['trace.id']))); + + await apmSynthtraceEsClient.index(Readable.from(unserialized)); + + return apmApiClient + .readUser({ + endpoint: 'POST /internal/apm/traces/aggregated_critical_path', + params: { + body: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + traceIds, + serviceName: 'serviceName' in options ? options.serviceName : null, + transactionName: 'transactionName' in options ? options.transactionName : null, + }, }, - }, - }) - .then((response) => { - const criticalPath = response.body.criticalPath!; + }) + .then((response) => { + const criticalPath = response.body.criticalPath!; - const nodeIdByOperationId = invert(criticalPath.operationIdByNodeId); + const nodeIdByOperationId = invert(criticalPath.operationIdByNodeId); - const { rootNodes, maxDepth } = getAggregatedCriticalPathRootNodes({ - criticalPath, - }); + const { rootNodes, maxDepth } = getAggregatedCriticalPathRootNodes({ + criticalPath, + }); + + function hydrateNode(node: Node): HydratedNode { + return { + ...node, + metadata: criticalPath.metadata[criticalPath.operationIdByNodeId[node.nodeId]], + children: node.children.map(hydrateNode), + }; + } - function hydrateNode(node: Node): HydratedNode { return { - ...node, - metadata: criticalPath.metadata[criticalPath.operationIdByNodeId[node.nodeId]], - children: node.children.map(hydrateNode), + rootNodes: rootNodes.map(hydrateNode), + maxDepth, + criticalPath, + nodeIdByOperationId, }; - } - - return { - rootNodes: rootNodes.map(hydrateNode), - maxDepth, - criticalPath, - nodeIdByOperationId, - }; - }); - } + }); + } + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + after(() => apmSynthtraceEsClient.clean()); - // FLAKY: https://github.com/elastic/kibana/issues/177542 - registry.when('Aggregated critical path', { config: 'basic', archives: [] }, () => { it('builds up the correct tree for a single transaction', async () => { const java = apm .service({ name: 'java', environment: 'production', agentName: 'java' }) @@ -427,7 +434,5 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, ]); }); - - after(() => apmSynthtraceEsClient.clean()); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/find_traces.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/find_traces.spec.ts new file mode 100644 index 0000000000000..b1bd8467456d9 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/find_traces.spec.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { TraceSearchType } from '@kbn/apm-plugin/common/trace_explorer'; +import { Environment } from '@kbn/apm-plugin/common/environment_rt'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import { sortBy } from 'lodash'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmApiError } from '../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { generateTrace } from './generate_trace'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + // for EQL sequences to work, events need a slight time offset, + // as ES will sort based on @timestamp. to acommodate this offset + // we also add a little bit of a buffer to the requested time range + const endWithOffset = end + 100000; + + describe('Find traces', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + async function fetchTraceSamples({ + query, + type, + environment, + }: { + query: string; + type: TraceSearchType; + environment: Environment; + }) { + return apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/find`, + params: { + query: { + query, + type, + start: new Date(start).toISOString(), + end: new Date(endWithOffset).toISOString(), + environment, + }, + }, + }); + } + + function fetchTraces(traceSamples: Array<{ traceId: string; transactionId: string }>) { + if (!traceSamples.length) { + return []; + } + + return Promise.all( + traceSamples.map(async ({ traceId, transactionId }) => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId }, + query: { + start: new Date(start).toISOString(), + end: new Date(endWithOffset).toISOString(), + entryTransactionId: transactionId, + }, + }, + }); + return response.body.traceItems.traceDocs; + }) + ); + } + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('when traces do not exist', () => { + it('handles empty state', async () => { + const response = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: ENVIRONMENT_ALL.value, + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + traceSamples: [], + }); + }); + }); + + describe('when traces exist', () => { + before(() => { + const java = apm + .service({ name: 'java', environment: 'production', agentName: 'java' }) + .instance('java'); + + const node = apm + .service({ name: 'node', environment: 'development', agentName: 'nodejs' }) + .instance('node'); + + const python = apm + .service({ name: 'python', environment: 'production', agentName: 'python' }) + .instance('python'); + + return apmSynthtraceEsClient.index( + timerange(start, end) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return [ + generateTrace(timestamp, [java, node]), + generateTrace(timestamp, [node, java], 'redis'), + generateTrace(timestamp, [python], 'redis'), + generateTrace(timestamp, [python, node, java], 'elasticsearch'), + generateTrace(timestamp, [java, python, node]), + ]; + }) + ); + }); + + describe('when using KQL', () => { + describe('and the query is empty', () => { + it('returns all trace samples', async () => { + const { + body: { traceSamples }, + } = await fetchTraceSamples({ + query: '', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', + }); + + expect(traceSamples.length).to.eql(5); + }); + }); + + describe('and query is set', () => { + it('returns the relevant traces', async () => { + const { + body: { traceSamples }, + } = await fetchTraceSamples({ + query: 'span.destination.service.resource:elasticsearch', + type: TraceSearchType.kql, + environment: 'ENVIRONMENT_ALL', + }); + + expect(traceSamples.length).to.eql(1); + }); + }); + }); + + describe('when using EQL', () => { + describe('and the query is invalid', () => { + it.skip('returns a 400', async function () { + try { + await fetchTraceSamples({ + query: '', + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + this.fail(); + } catch (error: unknown) { + const apiError = error as ApmApiError; + expect(apiError.res.status).to.eql(400); + } + }); + }); + + describe('and the query is set', () => { + it('returns the correct trace samples for transaction sequences', async () => { + const { + body: { traceSamples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ transaction where service.name == "java" ] + [ transaction where service.name == "node" ]`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(traceSamples); + + expect(traces.length).to.eql(2); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter((doc) => doc.processor.event === 'transaction') + .map((doc) => doc.service.name); + }); + + expect(mapped).to.eql([ + ['java', 'node'], + ['java', 'python', 'node'], + ]); + }); + }); + + it('returns the correct trace samples for join sequences', async () => { + const { + body: { traceSamples }, + } = await fetchTraceSamples({ + query: `sequence by trace.id + [ span where service.name == "java" ] by span.id + [ transaction where service.name == "python" ] by parent.id`, + type: TraceSearchType.eql, + environment: 'ENVIRONMENT_ALL', + }); + + const traces = await fetchTraces(traceSamples); + + expect(traces.length).to.eql(1); + + const mapped = traces.map((traceDocs) => { + return sortBy(traceDocs, '@timestamp') + .filter((doc) => doc.processor.event === 'transaction') + .map((doc) => doc.service.name); + }); + + expect(mapped).to.eql([['java', 'python', 'node']]); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/traces/generate_trace.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/generate_trace.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/traces/generate_trace.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/generate_trace.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/index.ts new file mode 100644 index 0000000000000..d54216b3f5d8f --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/index.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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Traces', () => { + loadTestFile(require.resolve('./large_trace/large_trace.spec.ts')); + loadTestFile(require.resolve('./critical_path.spec.ts')); + loadTestFile(require.resolve('./find_traces.spec.ts')); + loadTestFile(require.resolve('./span_details.spec.ts')); + loadTestFile(require.resolve('./top_traces.spec.ts')); + loadTestFile(require.resolve('./trace_by_id.spec.ts')); + loadTestFile(require.resolve('./transaction_details.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/large_trace/generate_large_trace.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/large_trace/generate_large_trace.ts diff --git a/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/large_trace/large_trace.spec.ts similarity index 86% rename from x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/large_trace/large_trace.spec.ts index 023db5c0d2ba0..7a0952b856c6b 100644 --- a/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/large_trace/large_trace.spec.ts @@ -14,8 +14,9 @@ import { import type { Client } from '@elastic/elasticsearch'; import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import expect from '@kbn/expect'; -import { ApmApiClient } from '../../../common/config'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmApiClient } from '../../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { generateLargeTrace } from './generate_large_trace'; const start = new Date('2023-01-01T00:00:00.000Z').getTime(); @@ -23,16 +24,17 @@ const end = new Date('2023-01-01T00:01:00.000Z').getTime() - 1; const rootTransactionName = 'Long trace'; const environment = 'long_trace_scenario'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/177660 - registry.when('Large trace', { config: 'basic', archives: [] }, () => { + describe('Large trace', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + describe('when the trace is large (>15.000 items)', () => { - before(() => { + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return generateLargeTrace({ start, end, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/span_details.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/span_details.spec.ts new file mode 100644 index 0000000000000..d5b2efc3479a6 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/span_details.spec.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { Readable } from 'stream'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + describe('Span details', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + async function fetchSpanDetails({ + traceId, + spanId, + parentTransactionId, + }: { + traceId: string; + spanId: string; + parentTransactionId?: string; + }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}/spans/{spanId}`, + params: { + path: { traceId, spanId }, + query: { + parentTransactionId, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await fetchSpanDetails({ + traceId: 'foo', + spanId: 'bar', + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({}); + }); + }); + + describe('when data is loaded', () => { + let traceId: string; + let spanId: string; + let parentTransactionId: string; + before(async () => { + const instanceJava = apm + .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) + .instance('instance-b'); + const events = timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + instanceJava + .transaction({ transactionName: 'GET /apple 🍏' }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceJava + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + .children( + instanceJava + .span({ + spanName: 'get_green_apple_🍏', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .timestamp(timestamp + 50) + .duration(900) + .success() + ), + ]; + }); + + const unserialized = Array.from(events); + + const entities = unserialized.flatMap((event) => event.serialize()); + + const span = entities.find((entity) => { + return entity['processor.event'] === 'span'; + }); + spanId = span?.['span.id']!; + parentTransactionId = span?.['parent.id']!; + traceId = span?.['trace.id']!; + + await apmSynthtraceEsClient.index(Readable.from(unserialized)); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('span details', () => { + let spanDetails: Awaited<ReturnType<typeof fetchSpanDetails>>['body']; + before(async () => { + const response = await fetchSpanDetails({ + traceId, + spanId, + parentTransactionId, + }); + expect(response.status).to.eql(200); + spanDetails = response.body; + }); + it('returns span details', () => { + expect(spanDetails.span?.span.name).to.eql('get_green_apple_🍏'); + expect(spanDetails.parentTransaction?.transaction.name).to.eql('GET /apple 🍏'); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/top_traces.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/top_traces.spec.ts new file mode 100644 index 0000000000000..1f2abda3d42f8 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/top_traces.spec.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import archives_metadata from '../constants/archives_metadata'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + + const archiveName = '8.0.0'; + const metadata = archives_metadata[archiveName]; + + // url parameters + const { start, end } = metadata; + + describe('Top traces', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces`, + params: { + query: { + start, + end, + kuery: '', + environment: 'ENVIRONMENT_ALL', + probability: 1, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.items.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let response: any; + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES[archiveName]); + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/traces', + params: { + query: { + start, + end, + kuery: '', + environment: 'ENVIRONMENT_ALL', + probability: 1, + }, + }, + }); + }); + + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES[archiveName]); + }); + + it('returns the correct status code', () => { + expect(response.status).to.be(200); + }); + + it('returns the correct number of buckets', () => { + expectSnapshot(response.body.items.length).toMatchInline(`59`); + }); + + it('returns the correct buckets', () => { + const sortedItems = sortBy(response.body.items, 'impact'); + + const firstItem = sortedItems[0]; + const lastItem = sortedItems[sortedItems.length - 1]; + + const groups = sortedItems.map((item) => item.key).slice(0, 5); + + expectSnapshot(sortedItems).toMatch(); + + expectSnapshot(firstItem).toMatchInline(` + Object { + "agentName": "ruby", + "averageResponseTime": 5664, + "impact": 0, + "key": Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#create", + }, + "serviceName": "opbeans-ruby", + "transactionName": "Api::OrdersController#create", + "transactionType": "request", + "transactionsPerMinute": 0.0166666666666667, + } + `); + + expectSnapshot(lastItem).toMatchInline(` + Object { + "agentName": "nodejs", + "averageResponseTime": 1077989.66666667, + "impact": 100, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "Process payment", + }, + "serviceName": "opbeans-node", + "transactionName": "Process payment", + "transactionType": "Worker", + "transactionsPerMinute": 0.7, + } + `); + + expectSnapshot(groups).toMatchInline(` + Array [ + Object { + "service.name": "opbeans-ruby", + "transaction.name": "Api::OrdersController#create", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#products", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#orders", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#product", + }, + Object { + "service.name": "opbeans-node", + "transaction.name": "POST /api", + }, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/trace_by_id.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/trace_by_id.spec.ts new file mode 100644 index 0000000000000..5599844e744b0 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/trace_by_id.spec.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { Readable } from 'stream'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + describe('Trace by ID', () => { + describe('Trace does not exist', () => { + it('handles empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId: 'foo' }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + entryTransactionId: 'foo', + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + traceItems: { + exceedsMax: false, + traceDocs: [], + errorDocs: [], + spanLinksCountById: {}, + traceDocsTotal: 0, + maxTraceItems: 5000, + }, + }); + }); + }); + + describe('Trace exists', () => { + let entryTransactionId: string; + let serviceATraceId: string; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const instanceJava = apm + .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) + .instance('instance-b'); + const events = timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + instanceJava + .transaction({ transactionName: 'GET /apple 🍏' }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceJava + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + .children( + instanceJava + .span({ + spanName: 'get_green_apple_🍏', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .timestamp(timestamp + 50) + .duration(900) + .success() + ), + ]; + }); + const unserialized = Array.from(events); + + const serialized = unserialized.flatMap((event) => event.serialize()); + + entryTransactionId = serialized[0]['transaction.id']!; + serviceATraceId = serialized[0]['trace.id']!; + + await apmSynthtraceEsClient.index(Readable.from(unserialized)); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('return trace', () => { + let traces: APIReturnType<'GET /internal/apm/traces/{traceId}'>; + before(async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId: serviceATraceId }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + entryTransactionId, + }, + }, + }); + + expect(response.status).to.eql(200); + traces = response.body; + }); + + it('returns some errors', () => { + expect(traces.traceItems.errorDocs.length).to.be.greaterThan(0); + expect(traces.traceItems.errorDocs[0].error.exception?.[0].message).to.eql( + '[ResponseError] index_not_found_exception' + ); + }); + + it('returns some trace docs', () => { + expect(traces.traceItems.traceDocs.length).to.be.greaterThan(0); + expect( + traces.traceItems.traceDocs.map((item) => { + if (item.span && 'name' in item.span) { + return item.span.name; + } + if (item.transaction && 'name' in item.transaction) { + return item.transaction.name; + } + }) + ).to.eql(['GET /apple 🍏', 'get_green_apple_🍏']); + }); + + it('returns entry transaction details', () => { + expect(traces.entryTransaction).to.not.be(undefined); + expect(traces.entryTransaction?.transaction.id).to.equal(entryTransactionId); + expect(traces.entryTransaction?.transaction.name).to.equal('GET /apple 🍏'); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/transaction_details.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/transaction_details.spec.ts new file mode 100644 index 0000000000000..e29006e29989c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/traces/transaction_details.spec.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { Readable } from 'stream'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + async function fetchTransactionDetails({ + traceId, + transactionId, + }: { + traceId: string; + transactionId: string; + }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}/transactions/{transactionId}`, + params: { + path: { + traceId, + transactionId, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + describe('Transaction details', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await fetchTransactionDetails({ + traceId: 'foo', + transactionId: 'bar', + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({}); + }); + }); + + describe('when data is loaded', () => { + let traceId: string; + let transactionId: string; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const instanceJava = apm + .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) + .instance('instance-b'); + const events = timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + instanceJava + .transaction({ transactionName: 'GET /apple 🍏' }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceJava + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + .children( + instanceJava + .span({ + spanName: 'get_green_apple_🍏', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .timestamp(timestamp + 50) + .duration(900) + .success() + ), + ]; + }); + + const unserialized = Array.from(events); + + const entities = unserialized.flatMap((event) => event.serialize()); + + const transaction = entities[0]; + transactionId = transaction?.['transaction.id']!; + traceId = transaction?.['trace.id']!; + + await apmSynthtraceEsClient.index(Readable.from(unserialized)); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('transaction details', () => { + let transactionDetails: Awaited<ReturnType<typeof fetchTransactionDetails>>['body']; + before(async () => { + const response = await fetchTransactionDetails({ + traceId, + transactionId, + }); + expect(response.status).to.eql(200); + transactionDetails = response.body; + }); + it('returns transaction details', () => { + expect(transactionDetails.transaction.name).to.eql('GET /apple 🍏'); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/transactions/transactions_groups_alerts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/transactions/transactions_groups_alerts.spec.ts index 6abefb559bc2d..07b19ecc0b2a5 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/transactions/transactions_groups_alerts.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/transactions/transactions_groups_alerts.spec.ts @@ -73,7 +73,10 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon return response.body as TransactionsGroupsMainStatistics; } - describe('Transaction groups alerts', () => { + describe('Transaction groups alerts', function () { + // fails on MKI, see https://github.com/elastic/kibana/issues/201531 + this.tags(['failsOnMKI']); + describe('when data is loaded', () => { const transactions = [ { diff --git a/x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts b/x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts index bd423762255a5..1cf8493347a6a 100644 --- a/x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/default_configs/stateful.config.base.ts @@ -124,7 +124,6 @@ export function createStatefulTestConfig<T extends DeploymentAgnosticCommonServi path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml'), ], }, - kbnTestServer: { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts deleted file mode 100644 index fd06ee9f95266..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts +++ /dev/null @@ -1,253 +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 { omit, sortBy } from 'lodash'; -import { type Node, NodeType } from '@kbn/apm-plugin/common/connections'; -import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import archives from '../../../common/fixtures/es_archiver/archives_metadata'; -import type { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - function getName(node: Node) { - return node.type === NodeType.service ? node.serviceName : node.dependencyName; - } - - registry.when( - 'Service overview dependencies when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - let response: { - status: number; - body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; - }; - - before(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, - params: { - path: { serviceName: 'opbeans-python' }, - query: { - start, - end, - numBuckets: 20, - environment: ENVIRONMENT_ALL.value, - }, - }, - }); - }); - - it('returns a successful response', () => { - expect(response.status).to.be(200); - }); - - it('returns at least one item', () => { - expect(response.body.serviceDependencies.length).to.be.greaterThan(0); - - expectSnapshot(response.body.serviceDependencies.length).toMatchInline(`4`); - - const { currentStats, ...firstItem } = sortBy( - response.body.serviceDependencies, - 'currentStats.impact' - ).reverse()[0]; - - expectSnapshot(firstItem.location).toMatchInline(` - Object { - "agentName": "dotnet", - "dependencyName": "opbeans:3000", - "environment": "production", - "id": "5948c153c2d8989f92a9c75ef45bb845f53e200d", - "serviceName": "opbeans-dotnet", - "type": "service", - } - `); - - expectSnapshot( - omit(currentStats, [ - 'errorRate.timeseries', - 'throughput.timeseries', - 'latency.timeseries', - 'totalTime.timeseries', - ]) - ).toMatchInline(` - Object { - "errorRate": Object { - "value": 0.163636363636364, - }, - "impact": 100, - "latency": Object { - "value": 1117085.74545455, - }, - "throughput": Object { - "value": 1.83333333333333, - }, - "totalTime": Object { - "value": 61439716, - }, - } - `); - }); - - it('returns the right names', () => { - const names = response.body.serviceDependencies.map((item) => getName(item.location)); - expectSnapshot(names.sort()).toMatchInline(` - Array [ - "elasticsearch", - "opbeans-dotnet", - "postgresql", - "redis", - ] - `); - }); - - it('returns the right service names', () => { - const serviceNames = response.body.serviceDependencies - .map((item) => - item.location.type === NodeType.service ? getName(item.location) : undefined - ) - .filter(Boolean); - - expectSnapshot(serviceNames.sort()).toMatchInline(` - Array [ - "opbeans-dotnet", - ] - `); - }); - - it('returns the right latency values', () => { - const latencyValues = sortBy( - response.body.serviceDependencies.map((item) => ({ - name: getName(item.location), - latency: item.currentStats.latency.value, - })), - 'name' - ); - - expectSnapshot(latencyValues).toMatchInline(` - Array [ - Object { - "latency": 9496.32291666667, - "name": "elasticsearch", - }, - Object { - "latency": 1117085.74545455, - "name": "opbeans-dotnet", - }, - Object { - "latency": 27826.9968314322, - "name": "postgresql", - }, - Object { - "latency": 1468.27242524917, - "name": "redis", - }, - ] - `); - }); - - it('returns the right throughput values', () => { - const throughputValues = sortBy( - response.body.serviceDependencies.map((item) => ({ - name: getName(item.location), - throughput: item.currentStats.throughput.value, - })), - 'name' - ); - - expectSnapshot(throughputValues).toMatchInline(` - Array [ - Object { - "name": "elasticsearch", - "throughput": 3.2, - }, - Object { - "name": "opbeans-dotnet", - "throughput": 1.83333333333333, - }, - Object { - "name": "postgresql", - "throughput": 52.6, - }, - Object { - "name": "redis", - "throughput": 40.1333333333333, - }, - ] - `); - }); - - it('returns the right impact values', () => { - const impactValues = sortBy( - response.body.serviceDependencies.map((item) => ({ - name: getName(item.location), - impact: item.currentStats.impact, - })), - 'name' - ); - - expectSnapshot(impactValues).toMatchInline(` - Array [ - Object { - "impact": 0, - "name": "elasticsearch", - }, - Object { - "impact": 100, - "name": "opbeans-dotnet", - }, - Object { - "impact": 71.0403531954737, - "name": "postgresql", - }, - Object { - "impact": 1.41447268043525, - "name": "redis", - }, - ] - `); - }); - - it('returns the right totalTime values', () => { - const totalTimeValues = sortBy( - response.body.serviceDependencies.map((item) => ({ - name: getName(item.location), - totalTime: item.currentStats.totalTime.value, - })), - 'name' - ); - - expectSnapshot(totalTimeValues).toMatchInline(` - Array [ - Object { - "name": "elasticsearch", - "totalTime": 911647, - }, - Object { - "name": "opbeans-dotnet", - "totalTime": 61439716, - }, - Object { - "name": "postgresql", - "totalTime": 43911001, - }, - Object { - "name": "redis", - "totalTime": 1767800, - }, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts deleted file mode 100644 index 751f772fb7507..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.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 { take } from 'lodash'; -import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import type { ApmServices } from '../../common/config'; - -export async function getServiceNodeIds({ - apmApiClient, - start, - end, - serviceName = 'opbeans-java', - count = 1, -}: { - apmApiClient: Awaited<ReturnType<ApmServices['apmApiClient']>>; - start: string; - end: string; - serviceName?: string; - count?: number; -}) { - const { body } = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, - params: { - path: { serviceName }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start, - end, - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - sortField: 'throughput', - sortDirection: 'desc', - }, - }, - }); - - return take(body.currentPeriod.map((item) => item.serviceNodeName).sort(), count); -} diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts deleted file mode 100644 index af28697a254c2..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts +++ /dev/null @@ -1,179 +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 type { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; -import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import type { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { getServiceNodeIds } from './get_service_node_ids'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - const serviceName = 'opbeans-java'; - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - interface Response { - status: number; - body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; - } - - registry.when( - 'Service overview instances detailed statistics when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - describe('fetching data without comparison', () => { - let response: Response; - let serviceNodeIds: string[]; - - beforeEach(async () => { - serviceNodeIds = await getServiceNodeIds({ - apmApiClient, - start, - end, - }); - - response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', - params: { - path: { serviceName }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start, - end, - numBuckets: 20, - transactionType: 'request', - serviceNodeIds: JSON.stringify(serviceNodeIds), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - }); - - it('returns a service node item', () => { - expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0); - expect(Object.values(response.body.previousPeriod)).to.eql(0); - }); - - it('returns statistics for each service node', async () => { - const item = response.body.currentPeriod[serviceNodeIds[0]]; - - expect(item?.cpuUsage?.some((point) => isFiniteNumber(point.y))).to.be(true); - expect(item?.memoryUsage?.some((point) => isFiniteNumber(point.y))).to.be(true); - expect(item?.errorRate?.some((point) => isFiniteNumber(point.y))).to.be(true); - expect(item?.throughput?.some((point) => isFiniteNumber(point.y))).to.be(true); - expect(item?.latency?.some((point) => isFiniteNumber(point.y))).to.be(true); - }); - - it('returns the right data', () => { - expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`); - - expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(` - Array [ - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", - ] - `); - - expectSnapshot(response.body).toMatch(); - }); - }); - - describe('fetching data with comparison', () => { - let response: Response; - let serviceNodeIds: string[]; - - beforeEach(async () => { - serviceNodeIds = await getServiceNodeIds({ - apmApiClient, - start, - end, - }); - response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', - params: { - path: { serviceName }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - numBuckets: 20, - transactionType: 'request', - serviceNodeIds: JSON.stringify(serviceNodeIds), - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - }); - - it('returns a service node item for current and previous periods', () => { - expect(Object.values(response.body.currentPeriod).length).to.be.greaterThan(0); - expect(Object.values(response.body.previousPeriod).length).to.be.greaterThan(0); - }); - - it('returns statistics for current and previous periods', () => { - const currentPeriodItem = response.body.currentPeriod[serviceNodeIds[0]]; - - function hasValidYCoordinate(point: Coordinate) { - return isFiniteNumber(point.y); - } - - expect(currentPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true); - expect(currentPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true); - expect(currentPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true); - expect(currentPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true); - expect(currentPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true); - - const previousPeriodItem = response.body.previousPeriod[serviceNodeIds[0]]; - - expect(previousPeriodItem?.cpuUsage?.some(hasValidYCoordinate)).to.be(true); - expect(previousPeriodItem?.memoryUsage?.some(hasValidYCoordinate)).to.be(true); - expect(previousPeriodItem?.errorRate?.some(hasValidYCoordinate)).to.be(true); - expect(previousPeriodItem?.throughput?.some(hasValidYCoordinate)).to.be(true); - expect(previousPeriodItem?.latency?.some(hasValidYCoordinate)).to.be(true); - }); - - it('returns the right data for current and previous periods', () => { - expectSnapshot(Object.values(response.body.currentPeriod).length).toMatchInline(`1`); - expectSnapshot(Object.values(response.body.previousPeriod).length).toMatchInline(`1`); - - expectSnapshot(Object.keys(response.body.currentPeriod)).toMatchInline(` - Array [ - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", - ] - `); - expectSnapshot(Object.keys(response.body.previousPeriod)).toMatchInline(` - Array [ - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", - ] - `); - - expectSnapshot(response.body).toMatch(); - }); - - it('matches x-axis on current period and previous period', () => { - const currentLatencyItems = response.body.currentPeriod[serviceNodeIds[0]]?.latency; - const previousLatencyItems = response.body.previousPeriod[serviceNodeIds[0]]?.latency; - - expect(currentLatencyItems?.map(({ x }) => x)).to.be.eql( - previousLatencyItems?.map(({ x }) => x) - ); - }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts deleted file mode 100644 index 369490ae06d44..0000000000000 --- a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts +++ /dev/null @@ -1,225 +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 { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { TraceSearchType } from '@kbn/apm-plugin/common/trace_explorer'; -import { Environment } from '@kbn/apm-plugin/common/environment_rt'; -import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { sortBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { ApmApiError } from '../../common/apm_api_supertest'; -import { generateTrace } from './generate_trace'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2022-01-01T00:00:00.000Z').getTime(); - const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - - // for EQL sequences to work, events need a slight time offset, - // as ES will sort based on @timestamp. to acommodate this offset - // we also add a little bit of a buffer to the requested time range - const endWithOffset = end + 100000; - - async function fetchTraceSamples({ - query, - type, - environment, - }: { - query: string; - type: TraceSearchType; - environment: Environment; - }) { - return apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/find`, - params: { - query: { - query, - type, - start: new Date(start).toISOString(), - end: new Date(endWithOffset).toISOString(), - environment, - }, - }, - }); - } - - function fetchTraces(traceSamples: Array<{ traceId: string; transactionId: string }>) { - if (!traceSamples.length) { - return []; - } - - return Promise.all( - traceSamples.map(async ({ traceId, transactionId }) => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId }, - query: { - start: new Date(start).toISOString(), - end: new Date(endWithOffset).toISOString(), - entryTransactionId: transactionId, - }, - }, - }); - return response.body.traceItems.traceDocs; - }) - ); - } - - registry.when('Find traces when traces do not exist', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await fetchTraceSamples({ - query: '', - type: TraceSearchType.kql, - environment: ENVIRONMENT_ALL.value, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - traceSamples: [], - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177543 - registry.when('Find traces when traces exist', { config: 'basic', archives: [] }, () => { - before(() => { - const java = apm - .service({ name: 'java', environment: 'production', agentName: 'java' }) - .instance('java'); - - const node = apm - .service({ name: 'node', environment: 'development', agentName: 'nodejs' }) - .instance('node'); - - const python = apm - .service({ name: 'python', environment: 'production', agentName: 'python' }) - .instance('python'); - - return apmSynthtraceEsClient.index( - timerange(start, end) - .interval('15m') - .rate(1) - .generator((timestamp) => { - return [ - generateTrace(timestamp, [java, node]), - generateTrace(timestamp, [node, java], 'redis'), - generateTrace(timestamp, [python], 'redis'), - generateTrace(timestamp, [python, node, java], 'elasticsearch'), - generateTrace(timestamp, [java, python, node]), - ]; - }) - ); - }); - - describe('when using KQL', () => { - describe('and the query is empty', () => { - it('returns all trace samples', async () => { - const { - body: { traceSamples }, - } = await fetchTraceSamples({ - query: '', - type: TraceSearchType.kql, - environment: 'ENVIRONMENT_ALL', - }); - - expect(traceSamples.length).to.eql(5); - }); - }); - - describe('and query is set', () => { - it('returns the relevant traces', async () => { - const { - body: { traceSamples }, - } = await fetchTraceSamples({ - query: 'span.destination.service.resource:elasticsearch', - type: TraceSearchType.kql, - environment: 'ENVIRONMENT_ALL', - }); - - expect(traceSamples.length).to.eql(1); - }); - }); - }); - - describe('when using EQL', () => { - describe('and the query is invalid', () => { - it.skip('returns a 400', async function () { - try { - await fetchTraceSamples({ - query: '', - type: TraceSearchType.eql, - environment: 'ENVIRONMENT_ALL', - }); - this.fail(); - } catch (error: unknown) { - const apiError = error as ApmApiError; - expect(apiError.res.status).to.eql(400); - } - }); - }); - - describe('and the query is set', () => { - it('returns the correct trace samples for transaction sequences', async () => { - const { - body: { traceSamples }, - } = await fetchTraceSamples({ - query: `sequence by trace.id - [ transaction where service.name == "java" ] - [ transaction where service.name == "node" ]`, - type: TraceSearchType.eql, - environment: 'ENVIRONMENT_ALL', - }); - - const traces = await fetchTraces(traceSamples); - - expect(traces.length).to.eql(2); - - const mapped = traces.map((traceDocs) => { - return sortBy(traceDocs, '@timestamp') - .filter((doc) => doc.processor.event === 'transaction') - .map((doc) => doc.service.name); - }); - - expect(mapped).to.eql([ - ['java', 'node'], - ['java', 'python', 'node'], - ]); - }); - }); - - it('returns the correct trace samples for join sequences', async () => { - const { - body: { traceSamples }, - } = await fetchTraceSamples({ - query: `sequence by trace.id - [ span where service.name == "java" ] by span.id - [ transaction where service.name == "python" ] by parent.id`, - type: TraceSearchType.eql, - environment: 'ENVIRONMENT_ALL', - }); - - const traces = await fetchTraces(traceSamples); - - expect(traces.length).to.eql(1); - - const mapped = traces.map((traceDocs) => { - return sortBy(traceDocs, '@timestamp') - .filter((doc) => doc.processor.event === 'transaction') - .map((doc) => doc.service.name); - }); - - expect(mapped).to.eql([['java', 'python', 'node']]); - }); - }); - - after(() => apmSynthtraceEsClient.clean()); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts b/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts deleted file mode 100644 index a428ea9cb2e50..0000000000000 --- a/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2022-01-01T00:00:00.000Z').getTime(); - const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - - async function fetchSpanDetails({ - traceId, - spanId, - parentTransactionId, - }: { - traceId: string; - spanId: string; - parentTransactionId?: string; - }) { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}/spans/{spanId}`, - params: { - path: { traceId, spanId }, - query: { - parentTransactionId, - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, - }, - }); - } - - registry.when('Span details dont exist', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await fetchSpanDetails({ - traceId: 'foo', - spanId: 'bar', - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177544 - registry.when('Span details', { config: 'basic', archives: [] }, () => { - let traceId: string; - let spanId: string; - let parentTransactionId: string; - before(async () => { - const instanceJava = apm - .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) - .instance('instance-b'); - const events = timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => { - return [ - instanceJava - .transaction({ transactionName: 'GET /apple 🍏' }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceJava - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - .children( - instanceJava - .span({ - spanName: 'get_green_apple_🍏', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .timestamp(timestamp + 50) - .duration(900) - .success() - ), - ]; - }); - - const unserialized = Array.from(events); - - const entities = unserialized.flatMap((event) => event.serialize()); - - const span = entities.find((entity) => { - return entity['processor.event'] === 'span'; - }); - spanId = span?.['span.id']!; - parentTransactionId = span?.['parent.id']!; - traceId = span?.['trace.id']!; - - await apmSynthtraceEsClient.index(Readable.from(unserialized)); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('span details', () => { - let spanDetails: Awaited<ReturnType<typeof fetchSpanDetails>>['body']; - before(async () => { - const response = await fetchSpanDetails({ - traceId, - spanId, - parentTransactionId, - }); - expect(response.status).to.eql(200); - spanDetails = response.body; - }); - it('returns span details', () => { - expect(spanDetails.span?.span.name).to.eql('get_green_apple_🍏'); - expect(spanDetails.parentTransaction?.transaction.name).to.eql('GET /apple 🍏'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts deleted file mode 100644 index b49133240c865..0000000000000 --- a/x-pack/test/apm_api_integration/tests/traces/top_traces.spec.ts +++ /dev/null @@ -1,140 +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 { sortBy } from 'lodash'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - // url parameters - const { start, end } = metadata; - - registry.when('Top traces when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces`, - params: { - query: { - start, - end, - kuery: '', - environment: 'ENVIRONMENT_ALL', - probability: 1, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.items.length).to.be(0); - }); - }); - - registry.when( - 'Top traces when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - let response: any; - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/traces', - params: { - query: { - start, - end, - kuery: '', - environment: 'ENVIRONMENT_ALL', - probability: 1, - }, - }, - }); - }); - - it('returns the correct status code', async () => { - expect(response.status).to.be(200); - }); - - it('returns the correct number of buckets', async () => { - expectSnapshot(response.body.items.length).toMatchInline(`81`); - }); - - it('returns the correct buckets', async () => { - const sortedItems = sortBy(response.body.items, 'impact'); - - const firstItem = sortedItems[0]; - const lastItem = sortedItems[sortedItems.length - 1]; - - const groups = sortedItems.map((item) => item.key).slice(0, 5); - - expectSnapshot(sortedItems).toMatch(); - - expectSnapshot(firstItem).toMatchInline(` - Object { - "agentName": "java", - "averageResponseTime": 1639, - "impact": 0, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doPost", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doPost", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - } - `); - - expectSnapshot(lastItem).toMatchInline(` - Object { - "agentName": "dotnet", - "averageResponseTime": 5963775, - "impact": 100, - "key": Object { - "service.name": "opbeans-dotnet", - "transaction.name": "GET Orders/Get", - }, - "serviceName": "opbeans-dotnet", - "transactionName": "GET Orders/Get", - "transactionType": "request", - "transactionsPerMinute": 0.633333333333333, - } - `); - - expectSnapshot(groups).toMatchInline(` - Array [ - Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doPost", - }, - Object { - "service.name": "opbeans-node", - "transaction.name": "POST /api/orders", - }, - Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id", - }, - Object { - "service.name": "opbeans-dotnet", - "transaction.name": "POST Orders/Post", - }, - Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product", - }, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts deleted file mode 100644 index de07f3664104c..0000000000000 --- a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts +++ /dev/null @@ -1,145 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2022-01-01T00:00:00.000Z').getTime(); - const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - - registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId: 'foo' }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - entryTransactionId: 'foo', - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - traceItems: { - exceedsMax: false, - traceDocs: [], - errorDocs: [], - spanLinksCountById: {}, - traceDocsTotal: 0, - maxTraceItems: 5000, - }, - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177545 - registry.when('Trace exists', { config: 'basic', archives: [] }, () => { - let entryTransactionId: string; - let serviceATraceId: string; - - before(async () => { - const instanceJava = apm - .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) - .instance('instance-b'); - const events = timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => { - return [ - instanceJava - .transaction({ transactionName: 'GET /apple 🍏' }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceJava - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - .children( - instanceJava - .span({ - spanName: 'get_green_apple_🍏', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .timestamp(timestamp + 50) - .duration(900) - .success() - ), - ]; - }); - const unserialized = Array.from(events); - - const serialized = unserialized.flatMap((event) => event.serialize()); - - entryTransactionId = serialized[0]['transaction.id']!; - serviceATraceId = serialized[0]['trace.id']!; - - await apmSynthtraceEsClient.index(Readable.from(unserialized)); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('return trace', () => { - let traces: APIReturnType<'GET /internal/apm/traces/{traceId}'>; - before(async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId: serviceATraceId }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - entryTransactionId, - }, - }, - }); - - expect(response.status).to.eql(200); - traces = response.body; - }); - - it('returns some errors', () => { - expect(traces.traceItems.errorDocs.length).to.be.greaterThan(0); - expect(traces.traceItems.errorDocs[0].error.exception?.[0].message).to.eql( - '[ResponseError] index_not_found_exception' - ); - }); - - it('returns some trace docs', () => { - expect(traces.traceItems.traceDocs.length).to.be.greaterThan(0); - expect( - traces.traceItems.traceDocs.map((item) => { - if (item.span && 'name' in item.span) { - return item.span.name; - } - if (item.transaction && 'name' in item.transaction) { - return item.transaction.name; - } - }) - ).to.eql(['GET /apple 🍏', 'get_green_apple_🍏']); - }); - - it('returns entry transaction details', () => { - expect(traces.entryTransaction).to.not.be(undefined); - expect(traces.entryTransaction?.transaction.id).to.equal(entryTransactionId); - expect(traces.entryTransaction?.transaction.name).to.equal('GET /apple 🍏'); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts b/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts deleted file mode 100644 index 3665bfd8e8ea6..0000000000000 --- a/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts +++ /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 { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2022-01-01T00:00:00.000Z').getTime(); - const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - - async function fetchTransactionDetails({ - traceId, - transactionId, - }: { - traceId: string; - transactionId: string; - }) { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}/transactions/{transactionId}`, - params: { - path: { - traceId, - transactionId, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, - }, - }); - } - - registry.when('Transaction details dont exist', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await fetchTransactionDetails({ - traceId: 'foo', - transactionId: 'bar', - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177546 - registry.when('Transaction details', { config: 'basic', archives: [] }, () => { - let traceId: string; - let transactionId: string; - before(async () => { - const instanceJava = apm - .service({ name: 'synth-apple', environment: 'production', agentName: 'java' }) - .instance('instance-b'); - const events = timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => { - return [ - instanceJava - .transaction({ transactionName: 'GET /apple 🍏' }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceJava - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - .children( - instanceJava - .span({ - spanName: 'get_green_apple_🍏', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .timestamp(timestamp + 50) - .duration(900) - .success() - ), - ]; - }); - - const unserialized = Array.from(events); - - const entities = unserialized.flatMap((event) => event.serialize()); - - const transaction = entities[0]; - transactionId = transaction?.['transaction.id']!; - traceId = transaction?.['trace.id']!; - - await apmSynthtraceEsClient.index(Readable.from(unserialized)); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('transaction details', () => { - let transactionDetails: Awaited<ReturnType<typeof fetchTransactionDetails>>['body']; - before(async () => { - const response = await fetchTransactionDetails({ - traceId, - transactionId, - }); - expect(response.status).to.eql(200); - transactionDetails = response.body; - }); - it('returns transaction details', () => { - expect(transactionDetails.transaction.name).to.eql('GET /apple 🍏'); - }); - }); - }); -} diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts index f7089db84985b..ca4e6414c0dfd 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts @@ -25,7 +25,7 @@ import { getCasesMetrics, updateCase, } from '../../../../../common/lib/api'; -import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -42,8 +42,6 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(metrics).to.eql({ mttr: null }); - - await deleteAllCaseItems(es); }); describe(CaseMetricsFeature.MTTR, () => { @@ -79,6 +77,8 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(metrics).to.eql({ mttr: null }); + + await deleteAllCaseItems(es); }); describe('closed and open cases from kbn archive', () => { @@ -92,6 +92,7 @@ export default ({ getService }: FtrProviderContext): void => { await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.3.0/all_cases_metrics.json' ); + await deleteAllCaseItems(es); }); @@ -119,6 +120,62 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe(CaseMetricsFeature.STATUS, () => { + it('responses with zeros if there are no cases', async () => { + const metrics = await getCasesMetrics({ + supertest, + features: [CaseMetricsFeature.STATUS], + }); + + expect(metrics).to.eql({ + status: { + closed: 0, + inProgress: 0, + open: 0, + }, + }); + }); + + it('should return case statuses', async () => { + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + ]); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const metrics = await getCasesMetrics({ + supertest, + features: [CaseMetricsFeature.STATUS], + }); + + expect(metrics.status).to.eql({ + open: 1, + inProgress: 1, + closed: 1, + }); + + await deleteAllCaseItems(es); + }); + }); + describe('rbac', () => { before(async () => { await kibanaServer.importExport.load( @@ -132,6 +189,7 @@ export default ({ getService }: FtrProviderContext): void => { 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.3.0/all_cases_metrics.json', { space: 'space1' } ); + await deleteAllCaseItems(es); }); @@ -139,29 +197,68 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: globalRead, - expectedMetrics: { mttr: 220 }, + expectedMetrics: { + mttr: 220, + status: { + closed: 3, + inProgress: 0, + open: 1, + }, + }, owners: ['securitySolutionFixture', 'observabilityFixture'], }, { user: superUser, - expectedMetrics: { mttr: 220 }, + expectedMetrics: { + mttr: 220, + status: { + closed: 3, + inProgress: 0, + open: 1, + }, + }, owners: ['securitySolutionFixture', 'observabilityFixture'], }, { user: secOnlyRead, - expectedMetrics: { mttr: 250 }, + expectedMetrics: { + mttr: 250, + status: { + closed: 2, + inProgress: 0, + open: 1, + }, + }, owners: ['securitySolutionFixture'], }, - { user: obsOnlyRead, expectedMetrics: { mttr: 160 }, owners: ['observabilityFixture'] }, + { + user: obsOnlyRead, + expectedMetrics: { + mttr: 160, + status: { + closed: 1, + inProgress: 0, + open: 0, + }, + }, + owners: ['observabilityFixture'], + }, { user: obsSecRead, - expectedMetrics: { mttr: 220 }, + expectedMetrics: { + mttr: 220, + status: { + closed: 3, + inProgress: 0, + open: 1, + }, + }, owners: ['securitySolutionFixture', 'observabilityFixture'], }, ]) { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: [CaseMetricsFeature.MTTR], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], auth: { user: scenario.user, space: 'space1', @@ -182,7 +279,7 @@ export default ({ getService }: FtrProviderContext): void => { // user should not be able to read cases at the appropriate space await getCasesMetrics({ supertest: supertestWithoutAuth, - features: [CaseMetricsFeature.MTTR], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], auth: { user: scenario.user, space: scenario.space, @@ -195,7 +292,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: [CaseMetricsFeature.MTTR], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], query: { owner: 'securitySolutionFixture', }, @@ -205,13 +302,20 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - expect(metrics).to.eql({ mttr: 250 }); + expect(metrics).to.eql({ + mttr: 250, + status: { + closed: 2, + inProgress: 0, + open: 1, + }, + }); }); it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: [CaseMetricsFeature.MTTR], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], query: { owner: ['securitySolutionFixture', 'observabilityFixture'], }, @@ -221,13 +325,20 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - expect(metrics).to.eql({ mttr: 250 }); + expect(metrics).to.eql({ + mttr: 250, + status: { + closed: 2, + inProgress: 0, + open: 1, + }, + }); }); it('should respect the owner filter when using range queries', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: [CaseMetricsFeature.MTTR], + features: [CaseMetricsFeature.MTTR, CaseMetricsFeature.STATUS], query: { from: '2022-04-20', to: '2022-04-30', @@ -238,7 +349,14 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - expect(metrics).to.eql({ mttr: 250 }); + expect(metrics).to.eql({ + mttr: 250, + status: { + closed: 2, + inProgress: 0, + open: 0, + }, + }); }); }); }); diff --git a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts index 2065d1307fbda..8f48bbb7d1bf4 100644 --- a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts +++ b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts @@ -65,12 +65,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be( integrationPolicyName ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( - `Agentless policy for ${integrationPolicyName}` - ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending'); }); it(`should show setup technology selector in edit mode`, async () => { @@ -97,7 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - await cisIntegration.navigateToEditIntegrationPage(); + await cisIntegration.navigateToEditAgentlessIntegrationPage(); await pageObjects.header.waitUntilLoadingHasFinished(); expect(await cisIntegration.showSetupTechnologyComponent()).to.be(true); diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_api_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_api_sanity.ts new file mode 100644 index 0000000000000..da4eb02a813c0 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_api_sanity.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 expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const queryBar = getService('queryBar'); + const pageObjects = getPageObjects(['common', 'header', 'cisAddIntegration', 'findings']); + + describe('Agentless Cloud - Sanity Tests', function () { + this.tags(['cloud_security_posture_ui_sanity']); + + it(`should have agentless agent findings for AWS provider`, async () => { + const findings = pageObjects.findings; + + await findings.navigateToLatestFindingsPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await queryBar.setQuery('agent.name: *agentless* and cloud.provider : "aws"'); + await queryBar.submitQuery(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const agentlessFindingsRowsCount = await findings + .createDataTableObject('latest_findings_table') + .getRowsCount(); + + expect(agentlessFindingsRowsCount).to.be.greaterThan(0); + }); + + it(`should have agentless agent findings for Azure provider`, async () => { + const findings = pageObjects.findings; + + await findings.navigateToLatestFindingsPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await queryBar.setQuery('agent.name: *agentless* and cloud.provider : "gcp"'); + await queryBar.submitQuery(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const agentlessFindingsRowsCount = await findings + .createDataTableObject('latest_findings_table') + .getRowsCount(); + + expect(agentlessFindingsRowsCount).to.be.greaterThan(0); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_sanity.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_sanity.ts deleted file mode 100644 index d1a27bf5d8c1d..0000000000000 --- a/x-pack/test/cloud_security_posture_functional/cloud_tests/agentless_sanity.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; -import expect from '@kbn/expect'; -import type { FtrProviderContext } from '../ftr_provider_context'; -// eslint-disable-next-line import/no-default-export -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects([ - 'common', - 'cspSecurity', - 'security', - 'header', - 'cisAddIntegration', - ]); - - const CIS_AWS_OPTION_TEST_ID = 'cisAwsTestId'; - - const AWS_SINGLE_ACCOUNT_TEST_ID = 'awsSingleTestId'; - - describe('Agentless cloud', function () { - let cisIntegration: typeof pageObjects.cisAddIntegration; - let cisIntegrationAws: typeof pageObjects.cisAddIntegration.cisAws; - - before(async () => { - cisIntegration = pageObjects.cisAddIntegration; - cisIntegrationAws = pageObjects.cisAddIntegration.cisAws; // Start the usage api mock server on port 8081 - }); - - after(async () => { - await pageObjects.cspSecurity.logout(); - }); - - it(`should create agentless-agent`, async () => { - const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( - CLOUD_CREDENTIALS_PACKAGE_VERSION - ); - - await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); - await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); - - await cisIntegration.inputIntegrationName(integrationPolicyName); - - await cisIntegration.selectSetupTechnology('agentless'); - await cisIntegration.selectAwsCredentials('direct'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - await cisIntegration.clickSaveButton(); - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(false); - - await cisIntegration.navigateToIntegrationCspList(); - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( - integrationPolicyName - ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( - `Agentless policy for ${integrationPolicyName}` - ); - }); - - it(`should create default agent-based agent`, async () => { - const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; - - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( - CLOUD_CREDENTIALS_PACKAGE_VERSION - ); - - await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); - await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); - - await cisIntegration.inputIntegrationName(integrationPolicyName); - - await cisIntegration.clickSaveButton(); - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(true); - - const agentPolicyName = await cisIntegration.getAgentBasedPolicyValue(); - - await cisIntegration.navigateToIntegrationCspList(); - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( - integrationPolicyName - ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be(agentPolicyName); - }); - }); -} diff --git a/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts b/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts index 80afb04563326..37e74d1d6ede5 100644 --- a/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts +++ b/x-pack/test/cloud_security_posture_functional/cloud_tests/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { + loadTestFile(require.resolve('./agentless_api_sanity')); loadTestFile(require.resolve('./dashboard_sanity')); loadTestFile(require.resolve('./benchmark_sanity')); loadTestFile(require.resolve('./findings_sanity')); diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts index 563507d705583..fe95c0887711d 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts @@ -218,6 +218,10 @@ export function AddCisIntegrationFormPageProvider({ await testSubjects.click('integrationNameLink'); }; + const navigateToEditAgentlessIntegrationPage = async () => { + await testSubjects.click('agentlessIntegrationNameLink'); + }; + const navigateToAddIntegrationKspmPage = async (space?: string) => { const options = space ? { @@ -485,7 +489,7 @@ export function AddCisIntegrationFormPageProvider({ await navigateToIntegrationCspList(); await PageObjects.header.waitUntilLoadingHasFinished(); - await navigateToEditIntegrationPage(); + await navigateToEditAgentlessIntegrationPage(); await PageObjects.header.waitUntilLoadingHasFinished(); // Fill out form to edit an agentless integration @@ -498,7 +502,7 @@ export function AddCisIntegrationFormPageProvider({ // Check if the Direct Access Key is updated package policy api with successful toast expect(await testSubjects.exists('policyUpdateSuccessToast')).to.be(true); - await navigateToEditIntegrationPage(); + await navigateToEditAgentlessIntegrationPage(); await PageObjects.header.waitUntilLoadingHasFinished(); }; @@ -511,12 +515,23 @@ export function AddCisIntegrationFormPageProvider({ return await integration.getVisibleText(); }; + const getFirstCspmIntegrationPageAgentlessIntegration = async () => { + const integration = await testSubjects.find('agentlessIntegrationNameLink'); + return await integration.getVisibleText(); + }; + const getFirstCspmIntegrationPageAgent = async () => { const agent = await testSubjects.find('agentPolicyNameLink'); // this is assuming that the agent was just created therefor should be the first element return await agent.getVisibleText(); }; + const getFirstCspmIntegrationPageAgentlessStatus = async () => { + const agent = await testSubjects.find('agentlessStatusBadge'); + // this is assuming that the agent was just created therefor should be the first element + return await agent.getVisibleText(); + }; + const getAgentBasedPolicyValue = async () => { const agentName = await testSubjects.find('createAgentPolicyNameField'); return await agentName.getAttribute('value'); @@ -568,10 +583,13 @@ export function AddCisIntegrationFormPageProvider({ testSubjectIds, inputIntegrationName, getFirstCspmIntegrationPageIntegration, + getFirstCspmIntegrationPageAgentlessIntegration, getFirstCspmIntegrationPageAgent, + getFirstCspmIntegrationPageAgentlessStatus, getAgentBasedPolicyValue, showSuccessfulToast, showSetupTechnologyComponent, navigateToEditIntegrationPage, + navigateToEditAgentlessIntegrationPage, }; } diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 42b60a0624a96..4b56601078da2 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -334,5 +334,31 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .expect(400); }); + + it('should return incoming data status for specified agents', async () => { + // force install the system package to override package verification + await supertest + .post(`/api/fleet/epm/packages/system/1.50.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + + const { body: apiResponse1 } = await supertest + .get(`/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2`) + .expect(200); + const { body: apiResponse2 } = await supertest + .get( + `/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2&pkgName=system&pkgVersion=1.50.0` + ) + .expect(200); + expect(apiResponse1).to.eql({ + items: [{ agent1: { data: false } }, { agent2: { data: false } }], + dataPreview: [], + }); + expect(apiResponse2).to.eql({ + items: [{ agent1: { data: false } }, { agent2: { data: false } }], + dataPreview: [], + }); + }); }); } diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts index 8922bca6d7fdf..995d26d5efc94 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts @@ -101,7 +101,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('edits to a by value lens panel are properly applied', async () => { await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.switchToVisualization('pie'); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); @@ -112,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('disables save to library button without visualize save permissions', async () => { await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); const saveButton = await testSubjects.find('lnsApp_saveButton'); expect(await saveButton.getAttribute('disabled')).to.equal('true'); await lens.saveAndReturn(); diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts index a974eb8c1284b..804790e7ee060 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts @@ -47,7 +47,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('edits to a by value lens panel are properly applied', async () => { await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.switchToVisualization('pie'); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('editing and saving a lens by value panel retains number of panels', async () => { const originalPanelCount = await dashboard.getPanelCount(); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.switchToVisualization('treemap'); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); @@ -71,7 +71,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const newTitle = 'look out library, here I come!'; const originalPanelCount = await dashboard.getPanelCount(); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.save(newTitle, false, true); await dashboard.waitForRenderComplete(); const newPanelCount = await dashboard.getPanelCount(); diff --git a/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts index 81fade0255cf7..df5860fd20a8b 100644 --- a/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // All panels should be editable. This will catch cases where an error does not create an error embeddable. const panelTitles = await dashboard.getPanelTitles(); for (const title of panelTitles) { - await dashboardPanelActions.expectExistsEditPanelAction(title, true); + await dashboardPanelActions.expectExistsEditPanelAction(title); } }); diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts b/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts index d5070b931b18f..78b34f1d55933 100644 --- a/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts @@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('by reference', () => { it('can add a custom time range to panel', async () => { - await dashboardPanelActions.legacySaveToLibrary('My by reference visualization'); + await dashboardPanelActions.saveToLibrary('My by reference visualization'); await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.enableCustomTimeRange(); await dashboardCustomizePanel.openDatePickerQuickMenu(); diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index 7d8456a9e81a8..19109ef3b76e0 100644 --- a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.setCustomPanelTitle('Custom title'); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacySaveToLibrary(getVisTitle(true)); + await dashboardPanelActions.saveToLibrary(getVisTitle(true)); await retry.tryForTime(500, async () => { // need to surround in 'retry' due to delays in HTML updates causing the title read to be behind const [newPanelTitle] = await dashboard.getPanelTitles(); @@ -113,7 +113,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('resetting description on a by reference panel sets it to the library title', async () => { await dashboardPanelActions.navigateToEditorFromFlyout(); - // legacySaveToLibrary UI cannot set description await lens.save( getVisTitle(true), false, @@ -142,7 +141,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.setCustomPanelTitle('Custom title'); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacyUnlinkFromLibrary('Custom title'); + await dashboardPanelActions.unlinkFromLibrary('Custom title'); const [newPanelTitle] = await dashboard.getPanelTitles(); expect(newPanelTitle).to.equal('Custom title'); }); @@ -151,7 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.setCustomPanelTitle(''); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacySaveToLibrary(getVisTitle(true)); + await dashboardPanelActions.saveToLibrary(getVisTitle(true)); await retry.tryForTime(500, async () => { // need to surround in 'retry' due to delays in HTML updates causing the title read to be behind const [newPanelTitle] = await dashboard.getPanelTitles(); @@ -160,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('unlinking a by reference panel without a custom title will keep the library title', async () => { - await dashboardPanelActions.legacyUnlinkFromLibrary(getVisTitle()); + await dashboardPanelActions.unlinkFromLibrary(getVisTitle()); const [newPanelTitle] = await dashboard.getPanelTitles(); expect(newPanelTitle).to.equal(getVisTitle()); }); diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 3d8bdc9c7d781..7a9a5e3b1a8c3 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -67,6 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' diff --git a/x-pack/test/functional/apps/index_management/index_template_wizard.ts b/x-pack/test/functional/apps/index_management/index_template_wizard.ts index d932b96d4f6a1..8103dfc0776db 100644 --- a/x-pack/test/functional/apps/index_management/index_template_wizard.ts +++ b/x-pack/test/functional/apps/index_management/index_template_wizard.ts @@ -70,7 +70,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that index mode callout is displayed const indexModeCalloutText = await testSubjects.getVisibleText('indexModeCallout'); expect(indexModeCalloutText).to.be( - 'The index.mode setting has been set to Standard within template Logistics. Any changes to index.mode set on this page will be overwritten by the Logistics selection.' + 'The index.mode setting has been set to Standard within the Logistics step. Any changes to index.mode set on this page will be overwritten by the Logistics selection.' ); // Click Next button diff --git a/x-pack/test/functional/apps/lens/group3/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/group3/add_to_dashboard.ts index 433fc2dbc943f..acf383fb946f4 100644 --- a/x-pack/test/functional/apps/lens/group3/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group3/add_to_dashboard.ts @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Average of bytes', '5,727.322'); - await dashboardPanelActions.expectNotLinkedToLibrary('New Lens from Modal', true); + await dashboardPanelActions.expectNotLinkedToLibrary('New Lens from Modal'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(1); @@ -82,10 +82,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Maximum of bytes', '19,986'); - await dashboardPanelActions.expectNotLinkedToLibrary( - 'Artistpreviouslyknownaslens Copy', - true - ); + await dashboardPanelActions.expectNotLinkedToLibrary('Artistpreviouslyknownaslens Copy'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(1); @@ -109,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Average of bytes', '5,727.322'); - await dashboardPanelActions.expectNotLinkedToLibrary('New Lens from Modal', true); + await dashboardPanelActions.expectNotLinkedToLibrary('New Lens from Modal'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(2); @@ -131,10 +128,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Maximum of bytes', '19,986'); - await dashboardPanelActions.expectNotLinkedToLibrary( - 'Artistpreviouslyknownaslens Copy', - true - ); + await dashboardPanelActions.expectNotLinkedToLibrary('Artistpreviouslyknownaslens Copy'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(2); @@ -147,7 +141,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Average of bytes', '5,727.322'); - await dashboardPanelActions.expectLinkedToLibrary('New by ref Lens from Modal', true); + await dashboardPanelActions.expectLinkedToLibrary('New by ref Lens from Modal'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(1); @@ -162,7 +156,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Maximum of bytes', '19,986'); - await dashboardPanelActions.expectLinkedToLibrary('Artistpreviouslyknownaslens by ref', true); + await dashboardPanelActions.expectLinkedToLibrary('Artistpreviouslyknownaslens by ref'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(1); @@ -186,7 +180,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Average of bytes', '5,727.322'); - await dashboardPanelActions.expectLinkedToLibrary('New Lens by ref from Modal', true); + await dashboardPanelActions.expectLinkedToLibrary('New Lens by ref from Modal'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(2); @@ -208,10 +202,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await lens.assertLegacyMetric('Maximum of bytes', '19,986'); - await dashboardPanelActions.expectLinkedToLibrary( - 'Artistpreviouslyknownaslens by ref 2', - true - ); + await dashboardPanelActions.expectLinkedToLibrary('Artistpreviouslyknownaslens by ref 2'); const panelCount = await dashboard.getPanelCount(); expect(panelCount).to.eql(2); diff --git a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts index 3790c22c377be..4ff6da617bbd3 100644 --- a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts +++ b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts @@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await elasticChart.setNewChartUiDebugFlag(true); - await dashboardPanelActions.legacySaveToLibrary('My by reference visualization'); + await dashboardPanelActions.saveToLibrary('My by reference visualization'); await dashboardPanelActions.clickInlineEdit(); @@ -138,6 +138,71 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await timeToVisualize.resetNewDashboard(); }); + it('should reset changes made to the previous chart with adHoc dataView created from dashboard', async () => { + await dashboard.navigateToApp(); + await dashboard.clickNewDashboard(); + + // it creates a XY histogram with a breakdown by ip + await lens.createAndAddLensFromDashboard({ useAdHocDataView: true }); + await elasticChart.setNewChartUiDebugFlag(true); + // now edit inline and remove the breakdown dimension + await dashboardPanelActions.clickInlineEdit(); + await lens.removeDimension('lnsXY_splitDimensionPanel'); + + log.debug('Cancels the changes'); + await testSubjects.click('cancelFlyoutButton'); + await dashboard.waitForRenderComplete(); + + const data = await lens.getCurrentChartDebugStateForVizType('xyVisChart'); + expect(data?.bars?.length).to.be.above(1); + // open the inline editor again and check that the breakdown is still there + await dashboardPanelActions.clickInlineEdit(); + expect(await testSubjects.exists('lnsXY_splitDimensionPanel')).to.be(true); + // exit via cancel again + await testSubjects.click('cancelFlyoutButton'); + }); + + it('should reset changes made to the previous chart created from dashboard', async () => { + await dashboardPanelActions.removePanel(); + + // it creates a XY histogram with a breakdown by ip + await lens.createAndAddLensFromDashboard({}); + + await dashboard.waitForRenderComplete(); + await elasticChart.setNewChartUiDebugFlag(true); + // now edit inline and remove the breakdown dimension + await dashboardPanelActions.clickInlineEdit(); + await lens.removeDimension('lnsXY_splitDimensionPanel'); + + log.debug('Cancels the changes'); + await testSubjects.click('cancelFlyoutButton'); + await dashboard.waitForRenderComplete(); + + const data = await lens.getCurrentChartDebugStateForVizType('xyVisChart'); + expect(data?.bars?.length).to.be.above(1); + // open the inline editor again and check that the breakdown is still there + await dashboardPanelActions.clickInlineEdit(); + expect(await testSubjects.exists('lnsXY_splitDimensionPanel')).to.be(true); + // exit via cancel again + await testSubjects.click('cancelFlyoutButton'); + }); + + it('should apply changes made in the inline editing panel', async () => { + // now delete the breakdown dimension and check that has been saved + await dashboardPanelActions.clickInlineEdit(); + await lens.removeDimension('lnsXY_splitDimensionPanel'); + + log.debug('Applies the changes'); + await testSubjects.click('applyFlyoutButton'); + await dashboard.waitForRenderComplete(); + + const data = await lens.getCurrentChartDebugStateForVizType('xyVisChart'); + expect(data?.bars?.length).to.eql(1); + // reset all things + await elasticChart.setNewChartUiDebugFlag(false); + await timeToVisualize.resetNewDashboard(); + }); + it('should allow adding an annotation', async () => { await loadExistingLens(); await lens.save('xyVisChart Copy', true, false, false, 'new'); diff --git a/x-pack/test/functional/apps/lens/group4/dashboard.ts b/x-pack/test/functional/apps/lens/group4/dashboard.ts index 670ee2cb22da5..0efdd026e05fd 100644 --- a/x-pack/test/functional/apps/lens/group4/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/dashboard.ts @@ -27,6 +27,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); const queryBar = getService('queryBar'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); async function clickInChart(x: number, y: number) { const el = await elasticChart.getCanvas(); @@ -228,11 +230,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await find.clickByButtonText('lnsPieVis'); await dashboardAddPanel.closeAddPanel(); - await panelActions.legacyUnlinkFromLibrary('lnsPieVis'); + await panelActions.unlinkFromLibrary('lnsPieVis'); }); it('save lens panel to embeddable library', async () => { - await panelActions.legacySaveToLibrary('lnsPieVis - copy', 'lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', 'lnsPieVis'); await dashboardAddPanel.clickOpenAddPanel(); await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); @@ -320,5 +322,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(windowHandlers[0]); } }); + + it('should add a drilldown to a Lens by-value chart', async () => { + await dashboard.navigateToApp(); + await dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + // add a drilldown to the pie chart + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await testSubjects.click('actionFactoryItem-OPEN_IN_DISCOVER_DRILLDOWN'); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.closeFlyout(); + await header.waitUntilLoadingHasFinished(); + + // check that the drilldown is working now + await clickInChart(5, 5); // hardcoded position of the slice, depends heavy on data and charts implementation + expect( + await find.existsByCssSelector('[data-test-subj^="embeddablePanelAction-D_ACTION"]') + ).to.be(true); + + // save the dashboard + await dashboard.saveDashboard('dashboardWithDrilldown'); + + // re-open the dashboard and check the drilldown is still there + await dashboard.navigateToApp(); + await dashboard.loadSavedDashboard('dashboardWithDrilldown'); + await header.waitUntilLoadingHasFinished(); + + await clickInChart(5, 5); // hardcoded position of the slice, depends heavy on data and charts implementation + expect( + await find.existsByCssSelector('[data-test-subj^="embeddablePanelAction-D_ACTION"]') + ).to.be(true); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts index 40169ef15ccfe..4227835b2227c 100644 --- a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts @@ -23,6 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const dashboardPanelActions = getService('dashboardPanelActions'); + const monacoEditor = getService('monacoEditor'); + const dashboardAddPanel = getService('dashboardAddPanel'); const filterBarService = getService('filterBar'); const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); @@ -58,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show the open button for a compatible saved visualization with annotations and reference line', async () => { await dashboard.switchToEditMode(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await header.waitUntilLoadingHasFinished(); await lens.createLayer('annotations'); await lens.waitForVisualization('xyVisChart'); @@ -88,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should bring both dashboard context and visualization context to discover', async () => { await dashboard.switchToEditMode(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); await savedQueryManagementComponent.closeSavedQueryManagementComponent(); @@ -139,5 +141,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.closeCurrentWindow(); await browser.switchToWindow(dashboardWindowHandle); }); + + it.skip('should bring visualization context to discover for Lens ES|QL panels', async () => { + // clear out the dashboard + await dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.removePanel(); + await queryBar.setQuery(''); + await queryBar.submitQuery(); + await filterBarService.removeAllFilters(); + + // Create a new panel + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL'); + await dashboardAddPanel.expectEditorMenuClosed(); + + const ESQL_QUERY = 'from logs* | stats maxB = max(bytes)'; + // Configure the ES|QL chart + await monacoEditor.setCodeEditorValue(ESQL_QUERY); + await testSubjects.click('ESQLEditor-run-query-button'); + await header.waitUntilLoadingHasFinished(); + + const lensQuery = await monacoEditor.getCodeEditorValue(); + expect(lensQuery).to.equal(ESQL_QUERY); + await testSubjects.click('applyFlyoutButton'); + + // Save the dashboard + await dashboard.clickQuickSave(); + await dashboard.clickCancelOutOfEditMode(); + + // check if it works correctly + await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + + const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); + await browser.switchToWindow(discoverWindowHandle); + + // wait to discover to load + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + // now check that all queries and filters are correctly transferred + const discoverQuery = await monacoEditor.getCodeEditorValue(); + expect(discoverQuery).to.equal(ESQL_QUERY); + // Filters and queries should not be carried over. + // There's currently a bug but in this test will check only the right thing + + await browser.closeCurrentWindow(); + await browser.switchToWindow(dashboardWindowHandle); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group6/error_handling.ts b/x-pack/test/functional/apps/lens/group6/error_handling.ts index 1b035fab63979..9ac57287feb0b 100644 --- a/x-pack/test/functional/apps/lens/group6/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group6/error_handling.ts @@ -108,7 +108,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.find('emptyPlaceholder'); await dashboard.switchToEditMode(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await timePicker.waitForNoDataPopover(); await timePicker.ensureHiddenNoDataPopover(); diff --git a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts index b6b441249f21f..bb39217bd8868 100644 --- a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts @@ -90,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('retains its saved object tags after save and return', async () => { - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.saveAndReturn(); await header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index bf799673c2491..966102852f634 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -117,7 +117,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const titles = await dashboard.getPanelTitles(); expect(titles[0]).to.be(`${visTitle} (converted)`); - await panelActions.expectNotLinkedToLibrary(titles[0], true); + await panelActions.expectNotLinkedToLibrary(titles[0]); await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.removePanel(); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index f4db890b26952..47eacb2604983 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -34,6 +34,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const browser = getService('browser'); const dashboardAddPanel = getService('dashboardAddPanel'); const queryBar = getService('queryBar'); + const dataViews = getService('dataViews'); const { common, header, timePicker, dashboard, timeToVisualize, unifiedSearch, share } = getPageObjects([ @@ -1486,10 +1487,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont title, redirectToOrigin, ignoreTimeFilter, + useAdHocDataView, }: { title?: string; redirectToOrigin?: boolean; ignoreTimeFilter?: boolean; + useAdHocDataView?: boolean; }) { log.debug(`createAndAddLens${title}`); const inViewMode = await dashboard.getIsInViewMode(); @@ -1502,6 +1505,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await this.goToTimeRange(); } + if (useAdHocDataView) { + await dataViews.createFromSearchBar({ name: '*stash*', adHoc: true }); + } + await this.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'average', @@ -2044,5 +2051,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return { maxWidth, maxHeight, minWidth, minHeight, aspectRatio }; }, + + async toggleDebug(enable: boolean = true) { + await browser.execute(`window.ELASTIC_LENS_LOGGER = arguments[0];`, enable); + }, }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 0a3d988fd750c..3d2d1004528d0 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -85,7 +85,6 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); const esSupertest = getService('esSupertest'); const kbnSupertest = getService('supertest'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); return { assertResponseStatusCode(expectedStatus: number, actualStatus: number, responseBody: object) { @@ -310,8 +309,37 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Indices deleted.'); }, + async deleteExpiredAnomalyDetectionData() { + log.debug('Deleting expired data ...'); + const { body, status } = await esSupertest.delete('/_ml/_delete_expired_data'); + this.assertResponseStatusCode(200, status, body); + log.debug('> Expired data deleted.'); + }, + + async cleanAnomalyDetection() { + await this.deleteAllAnomalyDetectionJobs(); + await this.deleteAllCalendars(); + await this.deleteAllFilters(); + await this.deleteAllAnnotations(); + await this.deleteExpiredAnomalyDetectionData(); + await this.syncSavedObjects(); + }, + + async cleanDataFrameAnalytics() { + await this.deleteAllDataFrameAnalyticsJobs(); + await this.syncSavedObjects(); + }, + + async cleanTrainedModels() { + await this.deleteAllTrainedModelsIngestPipelines(); + await this.deleteAllTrainedModelsES(); + await this.syncSavedObjects(); + }, + async cleanMlIndices() { - await esDeleteAllIndices('.ml-*'); + await this.cleanAnomalyDetection(); + await this.cleanDataFrameAnalytics(); + await this.cleanTrainedModels(); }, async getJobState(jobId: string): Promise<JOB_STATE> { @@ -537,6 +565,12 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return response; }, + async getAllCalendars(expectedCode = 200) { + const response = await esSupertest.get('/_ml/calendars/_all'); + this.assertResponseStatusCode(expectedCode, response.status, response.body); + return response; + }, + async createCalendar( calendarId: string, requestBody: Partial<Calendar> = { description: '', job_ids: [] } @@ -559,6 +593,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Calendar deleted.'); }, + async deleteAllCalendars() { + log.debug('Deleting all calendars'); + const getAllCalendarsRsp = await this.getAllCalendars(); + for (const calendar of getAllCalendarsRsp.body.calendars) { + await this.deleteCalendar(calendar.calendar_id); + } + }, + async waitForCalendarToExist(calendarId: string, errorMsg?: string) { await retry.waitForWithTimeout(`'${calendarId}' to exist`, 5 * 1000, async () => { if (await this.getCalendar(calendarId, 200)) { @@ -660,6 +702,12 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return response; }, + async getAllAnomalyDetectionJobs() { + const response = await esSupertest.get('/_ml/anomaly_detectors/_all'); + this.assertResponseStatusCode(200, response.status, response.body); + return response; + }, + async getAnomalyDetectionJobsKibana(jobId?: string, space?: string) { const { body, status } = await kbnSupertest .get( @@ -831,6 +879,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> AD job deleted.'); }, + async deleteAllAnomalyDetectionJobs() { + log.debug('Deleting all anomaly detection jobs'); + const getAllAdJobsResp = await this.getAllAnomalyDetectionJobs(); + for (const job of getAllAdJobsResp.body.jobs) { + await this.deleteAnomalyDetectionJobES(job.job_id); + } + }, + async getDatafeed(datafeedId: string) { const response = await esSupertest.get(`/_ml/datafeeds/${datafeedId}`); this.assertResponseStatusCode(200, response.status, response.body); @@ -1034,6 +1090,12 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> DFA job created.'); }, + async getAllDataFrameAnalyticsJobs(expectedCode = 200) { + const response = await esSupertest.get('/_ml/data_frame/analytics/_all'); + this.assertResponseStatusCode(expectedCode, response.status, response.body); + return response; + }, + async createDataFrameAnalyticsJobES(jobConfig: DataFrameAnalyticsConfig) { const { id: analyticsId, ...analyticsConfig } = jobConfig; log.debug(`Creating data frame analytic job with id '${analyticsId}' via ES API...`); @@ -1064,6 +1126,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> DFA job deleted.'); }, + async deleteAllDataFrameAnalyticsJobs() { + log.debug('Deleting all data frame analytics jobs'); + const getAllDfaJobsResp = await this.getAllDataFrameAnalyticsJobs(); + for (const job of getAllDfaJobsResp.body.data_frame_analytics) { + await this.deleteDataFrameAnalyticsJobES(job.id); + } + }, + async getADJobRecordCount(jobId: string): Promise<number> { const jobStats = await this.getADJobStats(jobId); @@ -1114,12 +1184,24 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, + async filterExists(filterId: string): Promise<boolean> { + const { status } = await esSupertest.get(`/_ml/filters/${filterId}`); + if (status !== 200) return false; + return true; + }, + async getFilter(filterId: string, expectedCode = 200) { const response = await esSupertest.get(`/_ml/filters/${filterId}`); this.assertResponseStatusCode(expectedCode, response.status, response.body); return response; }, + async getAllFilters(expectedCode = 200) { + const response = await esSupertest.get(`/_ml/filters`); + this.assertResponseStatusCode(expectedCode, response.status, response.body); + return response; + }, + async createFilter(filterId: string, requestBody: object) { log.debug(`Creating filter with id '${filterId}'...`); const { body, status } = await esSupertest.put(`/_ml/filters/${filterId}`).send(requestBody); @@ -1131,12 +1213,27 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async deleteFilter(filterId: string) { log.debug(`Deleting filter with id '${filterId}'...`); - await esSupertest.delete(`/_ml/filters/${filterId}`); + + if ((await this.filterExists(filterId)) === false) { + log.debug('> Filter does not exist, nothing to delete'); + return; + } + + const { body, status } = await esSupertest.delete(`/_ml/filters/${filterId}`); + this.assertResponseStatusCode(200, status, body); await this.waitForFilterToNotExist(filterId, `expected filter '${filterId}' to be deleted`); log.debug('> Filter deleted.'); }, + async deleteAllFilters() { + log.debug('Deleting all filters'); + const getAllFiltersRsp = await this.getAllFilters(); + for (const filter of getAllFiltersRsp.body.filters) { + await this.deleteFilter(filter.filter_id); + } + }, + async assertModelMemoryLimitForJob(jobId: string, expectedMml: string) { const { body: { @@ -1198,6 +1295,25 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return body.hits.hits; }, + async getAllAnnotations() { + log.debug('Fetching all annotations ...'); + + if ( + (await es.indices.exists({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + allow_no_indices: false, + })) === false + ) { + return []; + } + + const body = await es.search<Annotation>({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ }); + expect(body).to.not.be(undefined); + expect(body).to.have.property('hits'); + log.debug('> All annotations fetched.'); + return body.hits.hits; + }, + async getAnnotationById(annotationId: string): Promise<Annotation | undefined> { log.debug(`Fetching annotation '${annotationId}'...`); @@ -1264,6 +1380,24 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, + async deleteAnnotation(annotationId: string) { + log.debug(`Deleting annotation with id "${annotationId}"`); + const { body, status } = await kbnSupertest + .delete(`/internal/ml/annotations/delete/${annotationId}`) + .set(getCommonRequestHeader('1')); + this.assertResponseStatusCode(200, status, body); + + log.debug('> Annotation deleted'); + }, + + async deleteAllAnnotations() { + log.debug('Deleting all annotations.'); + const allAnnotations = await this.getAllAnnotations(); + for (const annotation of allAnnotations) { + await this.deleteAnnotation(annotation._id!); + } + }, + async runDFAJob(dfaId: string) { log.debug(`Starting data frame analytics job '${dfaId}'...`); const { body: startResponse, status } = await esSupertest @@ -1647,6 +1781,18 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Ingest pipeline deleted'); }, + async deleteAllTrainedModelsIngestPipelines() { + log.debug(`Deleting all trained models ingest pipelines`); + const getModelsRsp = await this.getTrainedModelsES(); + for (const model of getModelsRsp.trained_model_configs) { + if (this.isInternalModelId(model.model_id)) { + log.debug(`> Skipping internal ${model.model_id}`); + continue; + } + await this.deleteIngestPipeline(model.model_id); + } + }, + async assureMlStatsIndexExists(timeout: number = 60 * 1000) { const params = { index: '.ml-stats-000001', diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 010bae3ba4bb2..ab4dc572517ca 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -32,8 +32,6 @@ export default function ({ getService }: FtrProviderContext) { const expectedUploadFileTitle = 'artificial_server_log'; before(async () => { - await ml.api.cleanMlIndices(); - await esArchiver.loadIfNeeded( 'x-pack/test/functional/es_archives/ml/module_sample_ecommerce' ); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index 96aec074ec557..14a4be1ac8fdc 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -32,8 +32,6 @@ export default function ({ getService }: FtrProviderContext) { const expectedUploadFileTitle = 'artificial_server_log'; before(async () => { - await ml.api.cleanMlIndices(); - await esArchiver.loadIfNeeded( 'x-pack/test/functional/es_archives/ml/module_sample_ecommerce' ); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts index 25bbeb183a3b6..8965504aafc3c 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts @@ -30,7 +30,6 @@ export async function createKnowledgeBaseModel(ml: ReturnType<typeof MachineLear export async function deleteKnowledgeBaseModel(ml: ReturnType<typeof MachineLearningProvider>) { await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); await ml.api.deleteTrainedModelES(TINY_ELSER.id); - await ml.api.cleanMlIndices(); await ml.testResources.cleanMLSavedObjects(); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index a6bf7e7e9d5f2..e8e24f53551e8 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -143,6 +143,7 @@ export default function ({ getService }: FtrProviderContext) { 'endpoint:metadata-check-transforms-task', 'endpoint:user-artifact-packager', 'entity_store:field_retention:enrichment', + 'fleet:bump_agent_policies', 'fleet:check-deleted-files-task', 'fleet:delete-unenrolled-agents-task', 'fleet:deploy_agent_policies', diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts index b32eafc8c6899..0ce8303a291b3 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts @@ -48,7 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigating to lens and back should create a new session const byRefSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); const newByRefSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); @@ -56,12 +56,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(byRefSessionId).not.to.eql(newByRefSessionId); // Convert to by-value - await dashboardPanelActions.legacyUnlinkFromLibrary(lensTitle); + await dashboardPanelActions.unlinkFromLibrary(lensTitle); await dashboard.waitForRenderComplete(); const byValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); // Navigating to lens and back should keep the session - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); const newByValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.data_source_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.data_source_fields.ts index f3b009a8ade47..b95208856a275 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.data_source_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.data_source_fields.ts @@ -897,11 +897,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(fieldDiffObject.data_source).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -958,7 +958,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + tags + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // tags expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.eql_query_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.eql_query_fields.ts index f500f8691485b..eef3e4b6b7ce4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.eql_query_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.eql_query_fields.ts @@ -382,11 +382,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(fieldDiffObject.eql_query).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -451,7 +451,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // version + query - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.esql_query_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.esql_query_fields.ts index f77f59b16a3e1..9561393e84549 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.esql_query_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.esql_query_fields.ts @@ -356,11 +356,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(fieldDiffObject.esql_query).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -420,7 +420,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.kql_query_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.kql_query_fields.ts index 3afb63a4c7975..e8f9d2f48b9e0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.kql_query_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.kql_query_fields.ts @@ -1042,11 +1042,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(fieldDiffObject.kql_query).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -1114,7 +1114,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.multi_line_string_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.multi_line_string_fields.ts index d9c20fc28b43a..23bfd08f5b520 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.multi_line_string_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.multi_line_string_fields.ts @@ -386,7 +386,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(reviewResponse.rules[0].diff.fields.description).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); }); }); @@ -430,7 +430,7 @@ export default ({ getService }: FtrProviderContext): void => { has_base_version: false, }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.number_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.number_fields.ts index 51ab509d16904..bd059ec137a96 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.number_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.number_fields.ts @@ -274,11 +274,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(reviewResponse.rules[0].diff.fields.risk_score).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -322,7 +322,7 @@ export default ({ getService }: FtrProviderContext): void => { has_base_version: false, }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.rule_type_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.rule_type_fields.ts index 0764bac554bf5..3f6784108487c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.rule_type_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.rule_type_fields.ts @@ -298,11 +298,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(reviewResponse.rules[0].diff.fields.type).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered a conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -348,7 +348,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(3); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(3); // type + version + query are all considered conflicts + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // type + query are all considered conflicts expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.scalar_array_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.scalar_array_fields.ts index 503a51f84f812..881e8e6122175 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.scalar_array_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.scalar_array_fields.ts @@ -423,11 +423,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(reviewResponse.rules[0].diff.fields.tags).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -472,7 +472,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + tags + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // tags expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.single_line_string_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.single_line_string_fields.ts index b887e18813ec6..6fe10b9fa6012 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.single_line_string_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.single_line_string_fields.ts @@ -277,11 +277,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(reviewResponse.rules[0].diff.fields.name).toBeUndefined(); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered a conflict + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); - expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); }); }); @@ -326,7 +326,7 @@ export default ({ getService }: FtrProviderContext): void => { }); expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); - expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // name + version are both considered conflicts + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // name is considered as a conflict expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts index 0c5d86fa54800..5d0dcec11d9b6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts @@ -43,6 +43,19 @@ export default ({ getService }: FtrProviderContext) => { }); }); + describe('init error handling', () => { + afterEach(async () => { + await dataView.create('security-solution'); + await utils.cleanEngines(); + }); + + it('should return "error" when the security data view does not exist', async () => { + await dataView.delete('security-solution'); + await utils.initEntityEngineForEntityType('host'); + await utils.waitForEngineStatus('host', 'error'); + }); + }); + describe('enablement', () => { afterEach(async () => { await utils.cleanEngines(); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts index e94f7b7119ddf..716454bfbe169 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts @@ -10,7 +10,16 @@ export const dataViewRouteHelpersFactory = ( supertest: SuperTest.Agent, namespace: string = 'default' ) => ({ - create: (name: string) => { + create: async (name: string) => { + const { body: existingDataView, statusCode } = await supertest.get( + `/s/${namespace}/api/data_views/data_view/${name}-${namespace}` + ); + + if (statusCode === 200) { + // data view exists + return existingDataView; + } + return supertest .post(`/s/${namespace}/api/data_views/data_view`) .set('kbn-xsrf', 'foo') diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts index fff1040b81f29..0e7c94613010c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -51,7 +51,7 @@ export const EntityStoreUtils = ( } }; - const _initEntityEngineForEntityType = async (entityType: EntityType) => { + const initEntityEngineForEntityType = async (entityType: EntityType) => { log.info( `Initializing engine for entity type ${entityType} in namespace ${namespace || 'default'}` ); @@ -72,7 +72,7 @@ export const EntityStoreUtils = ( }; const initEntityEngineForEntityTypesAndWait = async (entityTypes: EntityType[]) => { - await Promise.all(entityTypes.map((entityType) => _initEntityEngineForEntityType(entityType))); + await Promise.all(entityTypes.map((entityType) => initEntityEngineForEntityType(entityType))); await retry.waitForWithTimeout( `Engines to start for entity types: ${entityTypes.join(', ')}`, @@ -90,6 +90,20 @@ export const EntityStoreUtils = ( ); }; + const waitForEngineStatus = async (entityType: EntityType, status: string) => { + await retry.waitForWithTimeout( + `Engine for entity type ${entityType} to be in status ${status}`, + 60_000, + async () => { + const { body } = await api + .getEntityEngine({ params: { entityType } }, namespace) + .expect(200); + log.debug(`Engine status for ${entityType}: ${body.status}`); + return body.status === status; + } + ); + }; + const enableEntityStore = async () => { const res = await api.initEntityStore({ body: {} }, namespace); if (res.status !== 200) { @@ -155,5 +169,7 @@ export const EntityStoreUtils = ( expectEngineAssetsExist, expectEngineAssetsDoNotExist, enableEntityStore, + waitForEngineStatus, + initEntityEngineForEntityType, }; }; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/eql_query_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/eql_query_rule.cy.ts index 994dbb2eb8ce8..d7bd6d8ebce77 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/eql_query_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/eql_query_rule.cy.ts @@ -15,7 +15,8 @@ import { } from '../../../../tasks/edit_rule'; import { login } from '../../../../tasks/login'; -describe('EQL query rules', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/201334 +describe.skip('EQL query rules', { tags: ['@ess', '@serverless'] }, () => { context('Editing rule with non-blocking query validation errors', () => { beforeEach(() => { login(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/inspect/inspect_button.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/inspect/inspect_button.cy.ts index a9025ba319acd..5f2b61b3de410 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/inspect/inspect_button.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/inspect/inspect_button.cy.ts @@ -44,8 +44,6 @@ describe('Inspect Explore pages', { tags: ['@ess', '@serverless'] }, () => { * Group all tests of a page into one "it" call to improve speed */ it(`inspect ${pageName} page`, () => { - login(); - visitWithTimeRange(url, { visitOptions: { onLoad: () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts index 1920bd01668f6..eba2dbe770e48 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts @@ -21,7 +21,6 @@ import { DETECTION_RESPONSE, DASHBOARDS, CSP_DASHBOARD, - KUBERNETES, INDICATORS, BLOCKLIST, CSP_BENCHMARKS, @@ -54,7 +53,6 @@ import { EXPLORE_URL, MANAGE_URL, CSP_DASHBOARD_URL, - KUBERNETES_URL, BLOCKLIST_URL, CSP_BENCHMARKS_URL, CSP_FINDINGS_URL, @@ -114,11 +112,6 @@ describe('top-level navigation common to all pages in the Security app', { tags: cy.url().should('include', ENTITY_ANALYTICS_URL); }); - it('navigates to the Kubernetes page', () => { - navigateFromHeaderTo(KUBERNETES); - cy.url().should('include', KUBERNETES_URL); - }); - it('navigates to the CSP dashboard page', () => { navigateFromHeaderTo(CSP_DASHBOARD); cy.url().should('include', CSP_DASHBOARD_URL); @@ -289,11 +282,6 @@ describe('Serverless side navigation links', { tags: '@serverless' }, () => { cy.url().should('include', ENTITY_ANALYTICS_URL); }); - it('navigates to the Kubernetes page', () => { - navigateFromHeaderTo(ServerlessHeaders.KUBERNETES, true); - cy.url().should('include', KUBERNETES_URL); - }); - it('navigates to the CSP dashboard page', () => { navigateFromHeaderTo(ServerlessHeaders.CSP_DASHBOARD, true); cy.url().should('include', CSP_DASHBOARD_URL); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts index 509ba40e57fc3..53db62751ebde 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts @@ -103,7 +103,8 @@ describe('KPI visualizations in Alerts Page', { tags: ['@ess', '@serverless'] }, }); }); - context('Histogram legend hover actions', () => { + // For some reason this suite is failing in CI while I cannot reproduce it locally + context.skip('Histogram legend hover actions', () => { it('should should add a filter in to KQL bar', () => { selectAlertsHistogram(); const expectedNumberOfAlerts = 1; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/data_streams.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/data_streams.ts index b6559c3efc9b6..d26b73f8689c8 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/data_streams.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/data_streams.ts @@ -33,7 +33,10 @@ export default function ({ getService }: FtrProviderContext) { await svlDatastreamsHelpers.deleteDataStream(testDataStreamName); }); - it('returns created data streams', async () => { + // skipped because we filter out data streams with 0 storage size, + // and metering api does not pick up indexed data here + // TODO: route should potentially not depend solely on metering API + it.skip('returns created data streams', async () => { const res = await supertestAdminWithCookieCredentials .get(DATA_USAGE_DATA_STREAMS_API_ROUTE) .set('elastic-api-version', '1'); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/traces/critical_path.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/traces/critical_path.ts deleted file mode 100644 index 50aab783b4cf5..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/traces/critical_path.ts +++ /dev/null @@ -1,99 +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 'expect'; -import { apm, ApmFields, SynthtraceGenerator, timerange } from '@kbn/apm-synthtrace-client'; -import { compact, uniq } from 'lodash'; -import { Readable } from 'stream'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { InternalRequestHeader, RoleCredentials } from '../../../../../shared/services'; -import { APMFtrContextProvider } from '../common/services'; - -export default function ({ getService }: APMFtrContextProvider) { - const apmApiClient = getService('apmApiClient'); - const svlUserManager = getService('svlUserManager'); - const svlCommonApi = getService('svlCommonApi'); - const synthtrace = getService('synthtrace'); - - const start = new Date('2022-01-01T00:00:00.000Z').getTime(); - const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - - async function fetchAndBuildCriticalPathTree( - synthtraceEsClient: ApmSynthtraceEsClient, - options: { - fn: () => SynthtraceGenerator<ApmFields>; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - } & ({ serviceName: string; transactionName: string } | {}) - ) { - const { fn, roleAuthc, internalReqHeader } = options; - - const generator = fn(); - - const unserialized = Array.from(generator); - const serialized = unserialized.flatMap((event) => event.serialize()); - const traceIds = compact(uniq(serialized.map((event) => event['trace.id']))); - - await synthtraceEsClient.index(Readable.from(unserialized)); - - return apmApiClient.slsUser({ - endpoint: 'POST /internal/apm/traces/aggregated_critical_path', - params: { - body: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - traceIds, - serviceName: 'serviceName' in options ? options.serviceName : null, - transactionName: 'transactionName' in options ? options.transactionName : null, - }, - }, - roleAuthc, - internalReqHeader, - }); - } - - describe('APM Aggregated critical path', () => { - let roleAuthc: RoleCredentials; - let internalReqHeader: InternalRequestHeader; - let synthtraceEsClient: ApmSynthtraceEsClient; - - before(async () => { - synthtraceEsClient = await synthtrace.createSynthtraceEsClient(); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - }); - - after(async () => { - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - return synthtraceEsClient.clean(); - }); - - it('returns service map elements', async () => { - const java = apm - .service({ name: 'java', environment: 'production', agentName: 'java' }) - .instance('java'); - - const duration = 1000; - const rate = 10; - - const response = await fetchAndBuildCriticalPathTree(synthtraceEsClient, { - fn: () => - timerange(start, end) - .interval('15m') - .rate(rate) - .generator((timestamp) => { - return java.transaction('GET /api').timestamp(timestamp).duration(duration); - }), - roleAuthc, - internalReqHeader, - }); - - expect(response.status).toBe(200); - expect(response.body.criticalPath).not.toBeUndefined(); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/config.ts index fa0714aa61544..e92609cc66ddc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/config.ts @@ -27,6 +27,7 @@ export default createTestConfig({ // useful for testing (also enabled in MKI QA) '--coreApp.allowDynamicConfigOverrides=true', '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 89543982f2d44..d3d8d4805695a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -12,7 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['esGate']); loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts')); - loadTestFile(require.resolve('./apm_api_integration/traces/critical_path')); loadTestFile(require.resolve('./cases')); loadTestFile(require.resolve('./synthetics')); loadTestFile(require.resolve('./dataset_quality_api_integration')); diff --git a/x-pack/test_serverless/api_integration/test_suites/search/config.ts b/x-pack/test_serverless/api_integration/test_suites/search/config.ts index 4db3e86bb9787..b662b105b54b5 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/config.ts @@ -23,6 +23,7 @@ export default createTestConfig({ // useful for testing (also enabled in MKI QA) '--coreApp.allowDynamicConfigOverrides=true', '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index 511ec3176ef6f..d5d816dfcdf17 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -27,6 +27,7 @@ export default createTestConfig({ `--xpack.securitySolutionServerless.cloudSecurityUsageReportingTaskInterval=5s`, `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081`, '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index 2c894b28b065f..e10e98529b8bf 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -25,6 +25,7 @@ import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsearch_start_page'; import { SvlApiKeysProvider } from './svl_api_keys'; import { SvlSearchCreateIndexPageProvider } from './svl_search_create_index_page'; +import { SvlSearchInferenceManagementPageProvider } from './svl_search_inference_management_page'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -47,4 +48,5 @@ export const pageObjects = { svlSearchElasticsearchStartPage: SvlSearchElasticsearchStartPageProvider, svlApiKeys: SvlApiKeysProvider, svlSearchCreateIndexPage: SvlSearchCreateIndexPageProvider, + svlSearchInferenceManagementPage: SvlSearchInferenceManagementPageProvider, }; diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_inference_management_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_inference_management_page.ts new file mode 100644 index 0000000000000..4424238a9c809 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/svl_search_inference_management_page.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 function SvlSearchInferenceManagementPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + + return { + InferenceTabularPage: { + async expectHeaderToBeExist() { + await testSubjects.existOrFail('allInferenceEndpointsPage'); + await testSubjects.existOrFail('api-documentation'); + await testSubjects.existOrFail('view-your-models'); + }, + + async expectTabularViewToBeLoaded() { + await testSubjects.existOrFail('search-field-endpoints'); + await testSubjects.existOrFail('type-field-endpoints'); + await testSubjects.existOrFail('service-field-endpoints'); + + const table = await testSubjects.find('inferenceEndpointTable'); + const rows = await table.findAllByClassName('euiTableRow'); + expect(rows.length).to.equal(2); + + const elserEndpointCell = await rows[0].findByTestSubject('endpointCell'); + const elserEndpointName = await elserEndpointCell.getVisibleText(); + expect(elserEndpointName).to.contain('.elser-2-elasticsearch'); + + const elserProviderCell = await rows[0].findByTestSubject('providerCell'); + const elserProviderName = await elserProviderCell.getVisibleText(); + expect(elserProviderName).to.contain('Elasticsearch'); + expect(elserProviderName).to.contain('.elser_model_2'); + + const elserTypeCell = await rows[0].findByTestSubject('typeCell'); + const elserTypeName = await elserTypeCell.getVisibleText(); + expect(elserTypeName).to.contain('sparse_embedding'); + + const e5EndpointCell = await rows[1].findByTestSubject('endpointCell'); + const e5EndpointName = await e5EndpointCell.getVisibleText(); + expect(e5EndpointName).to.contain('.multilingual-e5-small-elasticsearch'); + + const e5ProviderCell = await rows[1].findByTestSubject('providerCell'); + const e5ProviderName = await e5ProviderCell.getVisibleText(); + expect(e5ProviderName).to.contain('Elasticsearch'); + expect(e5ProviderName).to.contain('.multilingual-e5-small'); + + const e5TypeCell = await rows[1].findByTestSubject('typeCell'); + const e5TypeName = await e5TypeCell.getVisibleText(); + expect(e5TypeName).to.contain('text_embedding'); + }, + + async expectPreconfiguredEndpointsCannotBeDeleted() { + const table = await testSubjects.find('inferenceEndpointTable'); + const rows = await table.findAllByClassName('euiTableRow'); + + const elserDeleteAction = await rows[0].findByTestSubject('inferenceUIDeleteAction'); + const e5DeleteAction = await rows[1].findByTestSubject('inferenceUIDeleteAction'); + + expect(await elserDeleteAction.isEnabled()).to.be(false); + expect(await e5DeleteAction.isEnabled()).to.be(false); + }, + + async expectEndpointWithoutUsageTobeDelete() { + const table = await testSubjects.find('inferenceEndpointTable'); + const rows = await table.findAllByClassName('euiTableRow'); + + const userCreatedEndpoint = await rows[2].findByTestSubject('inferenceUIDeleteAction'); + + await userCreatedEndpoint.click(); + await testSubjects.existOrFail('deleteModalForInferenceUI'); + await testSubjects.existOrFail('deleteModalInferenceEndpointName'); + + await testSubjects.click('confirmModalConfirmButton'); + + await testSubjects.existOrFail('inferenceEndpointTable'); + }, + + async expectEndpointWithUsageTobeDelete() { + const table = await testSubjects.find('inferenceEndpointTable'); + const rows = await table.findAllByClassName('euiTableRow'); + + const userCreatedEndpoint = await rows[2].findByTestSubject('inferenceUIDeleteAction'); + + await userCreatedEndpoint.click(); + await testSubjects.existOrFail('deleteModalForInferenceUI'); + await testSubjects.existOrFail('deleteModalInferenceEndpointName'); + + const items = await testSubjects.findAll('usageItem'); + expect(items.length).to.equal(2); + + const index = await items[0].getVisibleText(); + const pipeline = await items[1].getVisibleText(); + + expect(index.includes('elser_index')).to.be(true); + expect(pipeline.includes('endpoint-1')).to.be(true); + + expect(await testSubjects.isEnabled('confirmModalConfirmButton')).to.be(false); + + await testSubjects.click('warningCheckbox'); + + expect(await testSubjects.isEnabled('confirmModalConfirmButton')).to.be(true); + await testSubjects.click('confirmModalConfirmButton'); + + await testSubjects.existOrFail('inferenceEndpointTable'); + }, + + async expectToCopyEndpoint() { + const table = await testSubjects.find('inferenceEndpointTable'); + const rows = await table.findAllByClassName('euiTableRow'); + + const elserCopyEndpointId = await rows[0].findByTestSubject( + 'inference-endpoints-action-copy-id-label' + ); + + await elserCopyEndpointId.click(); + expect((await browser.getClipboardValue()).includes('.elser-2-elasticsearch')).to.be(true); + }, + }, + }; +} diff --git a/x-pack/test_serverless/functional/services/svl_search_navigation.ts b/x-pack/test_serverless/functional/services/svl_search_navigation.ts index 1f27cf18ec8cb..434a5bd4e42ab 100644 --- a/x-pack/test_serverless/functional/services/svl_search_navigation.ts +++ b/x-pack/test_serverless/functional/services/svl_search_navigation.ts @@ -47,5 +47,10 @@ export function SvlSearchNavigationServiceProvider({ }); await testSubjects.existOrFail('searchIndicesDetailsPage', { timeout: 2000 }); }, + async navigateToInferenceManagementPage(expectRedirect: boolean = false) { + await PageObjects.common.navigateToApp('searchInferenceEndpoints', { + shouldLoginIfPrompted: false, + }); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts index ff0f2eadf3e20..6402b5ce4737c 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { savedSearchesRequests?: number; setQuery: (query: string) => Promise<void>; }) => { - it('should send 2 search requests (documents + chart) on page load', async () => { + it('should send no more than 2 search requests (documents + chart) on page load', async () => { await browser.refresh(); await browser.execute(async () => { performance.setResourceTimingBufferSize(Number.MAX_SAFE_INTEGER); @@ -105,20 +105,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(searchCount).to.be(2); }); - it('should send 2 requests (documents + chart) when refreshing', async () => { + it('should send no more than 2 requests (documents + chart) when refreshing', async () => { await expectSearches(type, 2, async () => { await queryBar.clickQuerySubmitButton(); }); }); - it('should send 2 requests (documents + chart) when changing the query', async () => { + it('should send no more than 2 requests (documents + chart) when changing the query', async () => { await expectSearches(type, 2, async () => { await setQuery(query1); await queryBar.clickQuerySubmitButton(); }); }); - it('should send 2 requests (documents + chart) when changing the time range', async () => { + it('should send no more than 2 requests (documents + chart) when changing the time range', async () => { await expectSearches(type, 2, async () => { await PageObjects.timePicker.setAbsoluteRange( 'Sep 21, 2015 @ 06:31:44.000', @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should send 2 requests (documents + chart) when toggling the chart visibility', async () => { + it('should send no more than 2 requests (documents + chart) when toggling the chart visibility', async () => { await expectSearches(type, 2, async () => { await PageObjects.discover.toggleChartVisibility(); }); @@ -136,7 +136,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should send 2 requests for saved search changes', async () => { + it('should send no more than 2 requests for saved search changes', async () => { await setQuery(query1); await queryBar.clickQuerySubmitButton(); await PageObjects.timePicker.setAbsoluteRange( @@ -181,7 +181,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { setQuery: (query) => queryBar.setQuery(query), }); - it('should send 2 requests (documents + chart) when adding a filter', async () => { + it('should send no more than 2 requests (documents + chart) when adding a filter', async () => { await expectSearches(type, 2, async () => { await filterBar.addFilter({ field: 'extension', @@ -191,31 +191,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should send 2 requests (documents + chart) when sorting', async () => { + it('should send no more than 2 requests (documents + chart) when sorting', async () => { await expectSearches(type, 2, async () => { await PageObjects.discover.clickFieldSort('@timestamp', 'Sort Old-New'); }); }); - it('should send 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => { + it('should send no more than 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => { await expectSearches(type, 2, async () => { await PageObjects.discover.chooseBreakdownField('type'); }); }); - it('should send 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => { + it('should send no more than 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => { await expectSearches(type, 3, async () => { await PageObjects.discover.chooseBreakdownField('extension.raw'); }); }); - it('should send 2 requests (documents + chart) when changing the chart interval', async () => { + it('should send no more than 2 requests (documents + chart) when changing the chart interval', async () => { await expectSearches(type, 2, async () => { await PageObjects.discover.setChartInterval('Day'); }); }); - it('should send 2 requests (documents + chart) when changing the data view', async () => { + it('should send no more than 2 requests (documents + chart) when changing the data view', async () => { await expectSearches(type, 2, async () => { await dataViews.switchToAndValidate('long-window-logstash-*'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/dashboard.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/dashboard.ts index eff79084e9dee..8b4a0019433b8 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/dashboard.ts @@ -113,7 +113,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const titles = await dashboard.getPanelTitles(); expect(titles[0]).to.be(`${visTitle} (converted)`); - await panelActions.expectNotLinkedToLibrary(titles[0], true); + await panelActions.expectNotLinkedToLibrary(titles[0]); await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); await panelActions.removePanel(); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.ts b/x-pack/test_serverless/functional/test_suites/observability/config.ts index 41093df640976..40ca0c915d2af 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/config.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/config.ts @@ -20,6 +20,7 @@ export default createTestConfig({ esServerArgs: ['xpack.ml.dfa.enabled=false'], kbnServerArgs: [ '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', diff --git a/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts index bdd5d443b3592..8073a7c5fcc78 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await ml.api.cleanMlIndices(); + await ml.api.cleanAnomalyDetection(); await ml.testResources.cleanMLSavedObjects(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.savedObjects.cleanStandardList(); diff --git a/x-pack/test_serverless/functional/test_suites/search/config.ts b/x-pack/test_serverless/functional/test_suites/search/config.ts index 5c52828a11659..dbf69f1c7091c 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.ts @@ -23,6 +23,7 @@ export default createTestConfig({ `--xpack.cloud.serverless.project_name=ES3_FTR_TESTS`, `--xpack.cloud.deployment_url=/projects/elasticsearch/fakeprojectid`, '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', @@ -45,5 +46,8 @@ export default createTestConfig({ elasticsearchIndices: { pathname: '/app/elasticsearch/indices', }, + searchInferenceEndpoints: { + pathname: '/app/elasticsearch/relevance/inference_endpoints', + }, }, }); diff --git a/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts index 8f97f53c6275f..aefd4c6da9832 100644 --- a/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts +++ b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts @@ -56,7 +56,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('can edit a Lens panel by value and save changes', async () => { await PageObjects.dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.navigateToEditorFromFlyout(); await PageObjects.lens.switchToVisualization('pie'); await PageObjects.lens.saveAndReturn(); await PageObjects.dashboard.waitForRenderComplete(); diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 99190ae0cc072..dd7021aebe800 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -28,5 +28,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./search_playground/playground_overview')); loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./custom_role_access')); + loadTestFile(require.resolve('./inference_management')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/inference_management.ts b/x-pack/test_serverless/functional/test_suites/search/inference_management.ts new file mode 100644 index 0000000000000..5293655ef092f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/inference_management.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { testHasEmbeddedConsole } from './embedded_console'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects([ + 'svlCommonPage', + 'embeddedConsole', + 'svlSearchInferenceManagementPage', + 'header', + ]); + const svlSearchNavigation = getService('svlSearchNavigation'); + const browser = getService('browser'); + const ml = getService('ml'); + + describe('Serverless Inference Management UI', function () { + const endpoint = 'endpoint-1'; + const taskType = 'sparse_embedding'; + const modelConfig = { + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + }, + }; + + before(async () => { + await pageObjects.svlCommonPage.loginWithRole('developer'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + beforeEach(async () => { + await svlSearchNavigation.navigateToInferenceManagementPage(); + }); + + describe('endpoint tabular view', () => { + it('is loaded successfully', async () => { + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectHeaderToBeExist(); + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectTabularViewToBeLoaded(); + }); + + it('preconfigured endpoints can not be deleted', async () => { + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectPreconfiguredEndpointsCannotBeDeleted(); + }); + }); + + describe('copy endpoint id action', () => { + it('can copy an endpoint id', async () => { + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectToCopyEndpoint(); + }); + }); + + describe('delete action', () => { + const usageIndex = 'elser_index'; + beforeEach(async () => { + await ml.api.createInferenceEndpoint(endpoint, taskType, modelConfig); + await browser.refresh(); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await ml.api.deleteIndices(usageIndex); + await ml.api.deleteIngestPipeline(endpoint); + }); + + it('deletes modal successfully without any usage', async () => { + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectEndpointWithoutUsageTobeDelete(); + }); + + it('deletes modal successfully with usage', async () => { + const indexMapping: estypes.MappingTypeMapping = { + properties: { + content: { + type: 'text', + }, + content_embedding: { + type: 'semantic_text', + inference_id: endpoint, + }, + }, + }; + await ml.api.createIngestPipeline(endpoint); + await ml.api.createIndex(usageIndex, indexMapping); + + await pageObjects.svlSearchInferenceManagementPage.InferenceTabularPage.expectEndpointWithUsageTobeDelete(); + }); + }); + + it('has embedded dev console', async () => { + await testHasEmbeddedConsole(pageObjects); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index 24009866b2b15..74d1c59cbc8e5 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -95,17 +95,17 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); // check Relevance // > Inference Endpoints - // await solutionNavigation.sidenav.clickLink({ - // deepLinkId: 'searchInferenceEndpoints', - // }); - // await solutionNavigation.sidenav.expectLinkActive({ - // deepLinkId: 'searchInferenceEndpoints', - // }); - // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Relevance' }); - // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Inference Endpoints' }); - // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - // deepLinkId: 'searchInferenceEndpoints', - // }); + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'searchInferenceEndpoints', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'searchInferenceEndpoints', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Relevance' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Inference Endpoints' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'searchInferenceEndpoints', + }); // check Analyze // > Discover @@ -245,8 +245,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await solutionNavigation.sidenav.expectLinkExists({ text: 'Build' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Dev Tools' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Playground' }); - // await solutionNavigation.sidenav.expectLinkExists({ text: 'Relevance' }); - // await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Relevance' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Analyze' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Discover' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Dashboards' }); @@ -270,8 +270,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { 'build', 'dev_tools', 'searchPlayground', - // 'relevance', - // 'searchInferenceEndpoints', + 'relevance', + 'searchInferenceEndpoints', 'analyze', 'discover', 'dashboards', diff --git a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts index 946afe08a0b73..9e1e36d10b176 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts @@ -106,7 +106,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('without any indices', () => { - it('hide no create index button when index added', async () => { + it('hide create index button when index added', async () => { await createIndex(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenFlyoutAndSelectIndex(); }); @@ -121,6 +121,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await createConnector(); await createIndex(); + }); + + beforeEach(async () => { await pageObjects.searchPlayground.session.setSession(); await browser.refresh(); }); @@ -129,6 +132,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndLoadChat(); }); + it('load start page after removing selected index', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndLoadChat(); + await esArchiver.unload(esArchiveIndex); + await browser.refresh(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectCreateIndexButtonToExists(); + }); + after(async () => { await removeOpenAIConnector?.(); await esArchiver.unload(esArchiveIndex); diff --git a/x-pack/test_serverless/functional/test_suites/security/config.ts b/x-pack/test_serverless/functional/test_suites/security/config.ts index 6bf456e5f6d55..0a6a3a061d1f8 100644 --- a/x-pack/test_serverless/functional/test_suites/security/config.ts +++ b/x-pack/test_serverless/functional/test_suites/security/config.ts @@ -20,6 +20,7 @@ export default createTestConfig({ esServerArgs: ['xpack.ml.nlp.enabled=true'], kbnServerArgs: [ '--xpack.dataUsage.enabled=true', + '--xpack.dataUsage.enableExperimental=[]', // dataUsage.autoops* config is set in kibana controller '--xpack.dataUsage.autoops.enabled=true', '--xpack.dataUsage.autoops.api.url=http://localhost:9000', diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts index 7611061398f18..017ef1b2fa82a 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts @@ -78,12 +78,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be( integrationPolicyName ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( - `Agentless policy for ${integrationPolicyName}` - ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending'); }); it(`should create default agent-based agent`, async () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts index 9e1154ea09bbc..e8f3f5e1796f3 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await ml.api.cleanMlIndices(); + await ml.api.cleanAnomalyDetection(); await ml.testResources.cleanMLSavedObjects(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.savedObjects.cleanStandardList(); diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts index d3caa3425f753..110cf64e07a17 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await ml.api.cleanMlIndices(); + await ml.api.cleanAnomalyDetection(); await ml.testResources.cleanMLSavedObjects(); }); diff --git a/yarn.lock b/yarn.lock index e3c2abbe30a7e..a24a9d81e476d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,15 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@apidevtools/json-schema-ref-parser@9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" + integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + js-yaml "^3.13.1" + "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" @@ -45,7 +54,7 @@ call-me-maybe "^1.0.1" js-yaml "^4.1.0" -"@apidevtools/openapi-schemas@^2.0.4": +"@apidevtools/openapi-schemas@^2.0.4", "@apidevtools/openapi-schemas@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== @@ -55,7 +64,7 @@ resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== -"@apidevtools/swagger-parser@10.0.3", "@apidevtools/swagger-parser@^10.0.3": +"@apidevtools/swagger-parser@10.0.3": version "10.0.3" resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== @@ -67,6 +76,19 @@ call-me-maybe "^1.0.1" z-schema "^5.0.1" +"@apidevtools/swagger-parser@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz#a987d71e5be61feb623203be0c96e5985b192ab6" + integrity sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.6" + "@apidevtools/openapi-schemas" "^2.1.0" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + ajv "^8.6.3" + ajv-draft-04 "^1.0.0" + call-me-maybe "^1.0.1" + "@appland/sql-parser@^1.5.1": version "1.5.1" resolved "https://registry.yarnpkg.com/@appland/sql-parser/-/sql-parser-1.5.1.tgz#331d644364899858ba7aa6e884e2492596990626" @@ -2033,7 +2055,7 @@ find-test-names "^1.19.0" globby "^11.0.4" -"@cypress/request@^3.0.0": +"@cypress/request@^3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.6.tgz#f5580add6acee0e183b4d4e07eff4f31327ae12b" integrity sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg== @@ -5298,6 +5320,10 @@ version "0.0.0" uid "" +"@kbn/dependency-usage@link:packages/kbn-dependency-usage": + version "0.0.0" + uid "" + "@kbn/dev-cli-errors@link:packages/kbn-dev-cli-errors": version "0.0.0" uid "" @@ -5430,6 +5456,10 @@ version "0.0.0" uid "" +"@kbn/entityManager-app-plugin@link:x-pack/plugins/observability_solution/entity_manager_app": + version "0.0.0" + uid "" + "@kbn/entityManager-plugin@link:x-pack/plugins/entity_manager": version "0.0.0" uid "" @@ -6846,6 +6876,10 @@ version "0.0.0" uid "" +"@kbn/scout@link:packages/kbn-scout": + version "0.0.0" + uid "" + "@kbn/screenshot-mode-example-plugin@link:examples/screenshot_mode_example": version "0.0.0" uid "" @@ -6914,6 +6948,10 @@ version "0.0.0" uid "" +"@kbn/search-navigation@link:x-pack/plugins/search_solution/search_navigation": + version "0.0.0" + uid "" + "@kbn/search-notebooks@link:x-pack/plugins/search_notebooks": version "0.0.0" uid "" @@ -7470,6 +7508,10 @@ version "0.0.0" uid "" +"@kbn/streams-app-plugin@link:x-pack/plugins/streams_app": + version "0.0.0" + uid "" + "@kbn/streams-plugin@link:x-pack/plugins/streams": version "0.0.0" uid "" @@ -10794,10 +10836,10 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz#50484da3f80fb222a853479f618a9ce5c47bfe54" - integrity sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA== +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== dependencies: "@adobe/css-tools" "^4.4.0" aria-query "^5.0.0" @@ -13099,11 +13141,23 @@ acorn-import-attributes@^1.9.5: resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== +acorn-jsx-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz#a5ed648264e68282d7c2aead80216bfdf232573a" + integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== + acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-loose@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + acorn-node@^1.6.1: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" @@ -13118,10 +13172,12 @@ acorn-walk@^7.0.0, acorn-walk@^7.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.4: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" acorn@^6.4.1: version "6.4.2" @@ -13133,10 +13189,10 @@ acorn@^7.0.0, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.1, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2, acorn@^8.9.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== address@^1.0.1: version "1.1.2" @@ -13227,6 +13283,11 @@ airbnb-js-shims@^2.2.1: string.prototype.padstart "^3.0.0" symbol.prototype.description "^1.0.0" +ajv-draft-04@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + ajv-errors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" @@ -13261,15 +13322,15 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0, ajv@^8.17.1, ajv@^8.6.3, ajv@^8.8.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" ansi-align@^3.0.0: version "3.0.1" @@ -13430,22 +13491,6 @@ arch@^2.2.0: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - archiver-utils@^5.0.0, archiver-utils@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" @@ -13459,19 +13504,6 @@ archiver-utils@^5.0.0, archiver-utils@^5.0.2: normalize-path "^3.0.0" readable-stream "^4.0.0" -archiver@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" - integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.3" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - archiver@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" @@ -14611,16 +14643,16 @@ buffer-builder@^0.2.0: resolved "https://registry.yarnpkg.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f" integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg== -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - buffer-crc32@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -14650,7 +14682,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -15119,10 +15151,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^130.0.4: - version "130.0.4" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-130.0.4.tgz#55225ddfec428e306116507651f5a24fdb299bd6" - integrity sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ== +chromedriver@^131.0.0: + version "131.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-131.0.1.tgz#bfbf47f6c2ad7a65c154ff47d321bd8c33b52a77" + integrity sha512-LHRh+oaNU1WowJjAkWsviN8pTzQYJDbv/FvJyrQ7XhjKdIzVh/s3GV1iU7IjMTsxIQnBsTjx+9jWjzCWIXC7ug== dependencies: "@testim/chrome-version" "^1.1.4" axios "^1.7.4" @@ -15151,6 +15183,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" + integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -15594,16 +15631,6 @@ component-emitter@^1.2.1, component-emitter@^1.3.0: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compress-commons@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" - integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.2" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - compress-commons@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" @@ -15890,14 +15917,6 @@ crc-32@^1.2.0: exit-on-epipe "~1.0.1" printj "~1.1.0" -crc32-stream@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" - integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== - dependencies: - crc-32 "^1.2.0" - readable-stream "^3.4.0" - crc32-stream@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" @@ -16282,22 +16301,23 @@ cypress-recurse@^1.35.2: dependencies: humanize-duration "^3.27.3" -cypress@13.6.3: - version "13.6.3" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.3.tgz#54f03ca07ee56b2bc18211e7bd32abd2533982ba" - integrity sha512-d/pZvgwjAyZsoyJ3FOsJT5lDsqnxQ/clMqnNc++rkHjbkkiF2h9s0JsZSyyH4QXhVFW3zPFg82jD25roFLOdZA== +cypress@13.15.2: + version "13.15.2" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.15.2.tgz#ef19554c274bc4ff23802aeb5c52951677fa67f1" + integrity sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A== dependencies: - "@cypress/request" "^3.0.0" + "@cypress/request" "^3.0.6" "@cypress/xvfb" "^1.2.4" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" blob-util "^2.0.2" bluebird "^3.7.2" - buffer "^5.6.0" + buffer "^5.7.1" cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" + ci-info "^4.0.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" commander "^6.2.1" @@ -16312,7 +16332,6 @@ cypress@13.6.3: figures "^3.2.0" fs-extra "^9.1.0" getos "^3.2.1" - is-ci "^3.0.0" is-installed-globally "~0.4.0" lazy-ass "^1.6.0" listr2 "^3.8.3" @@ -16326,7 +16345,8 @@ cypress@13.6.3: request-progress "^3.0.0" semver "^7.5.3" supports-color "^8.1.1" - tmp "~0.2.1" + tmp "~0.2.3" + tree-kill "1.2.2" untildify "^4.0.0" yauzl "^2.10.0" @@ -17011,6 +17031,34 @@ dependency-check@^4.1.0: read-package-json "^2.0.10" resolve "^1.1.7" +dependency-cruiser@^16.4.2: + version "16.4.2" + resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-16.4.2.tgz#586487e1ac355912a0ad2310b830b63054733e01" + integrity sha512-mQZM95WwIvKzYYdj+1RgIBuJ6qbr1cfyzTt62dDJVrWAShfhV9IEkG/Xv4S2iD5sT+Gt3oFWyZjwNufAhcbtWA== + dependencies: + acorn "^8.12.1" + acorn-jsx "^5.3.2" + acorn-jsx-walk "^2.0.0" + acorn-loose "^8.4.0" + acorn-walk "^8.3.4" + ajv "^8.17.1" + commander "^12.1.0" + enhanced-resolve "^5.17.1" + ignore "^6.0.2" + interpret "^3.1.1" + is-installed-globally "^1.0.0" + json5 "^2.2.3" + memoize "^10.0.0" + picocolors "^1.1.0" + picomatch "^4.0.2" + prompts "^2.4.2" + rechoir "^0.8.0" + safe-regex "^2.1.1" + semver "^7.6.3" + teamcity-service-messages "^0.1.14" + tsconfig-paths-webpack-plugin "^4.1.0" + watskeburt "^4.1.0" + dependency-tree@^10.0.9: version "10.0.9" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7" @@ -17709,10 +17757,10 @@ enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enhanced-resolve@^5.14.1, enhanced-resolve@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +enhanced-resolve@^5.14.1, enhanced-resolve@^5.16.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -18809,6 +18857,11 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + fast-xml-parser@4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" @@ -19818,6 +19871,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl once "^1.3.0" path-is-absolute "^1.0.0" +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + dependencies: + ini "4.1.1" + global-dirs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" @@ -20828,6 +20888,11 @@ ignore@^5.0.5, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.3.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +ignore@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-6.0.2.tgz#77cccb72a55796af1b6d2f9eb14fa326d24f4283" + integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -20924,6 +20989,11 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + ini@^1.3.5, ini@~1.3.0: version "1.3.7" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" @@ -21011,6 +21081,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + intl-messageformat@10.5.12: version "10.5.12" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.12.tgz#a0c1a20da896b7a1f4ba1b59c8ba5d9943c29c3f" @@ -21051,11 +21126,6 @@ ip-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== -ip@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" - integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== - ip@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" @@ -21185,13 +21255,6 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.13.0: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" @@ -21340,6 +21403,14 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" integrity sha1-bghLvJIGH7sJcexYts5tQE4k2mk= +is-installed-globally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-1.0.0.tgz#08952c43758c33d815692392f7f8437b9e436d5a" + integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== + dependencies: + global-directory "^4.0.1" + is-path-inside "^4.0.0" + is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -21456,6 +21527,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + is-plain-obj@2.1.0, is-plain-obj@^2.0.0, is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -23176,26 +23252,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -23296,11 +23357,6 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - lodash.uniq@4.5.0, lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -23839,6 +23895,13 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +memoize@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.0.0.tgz#43fa66b2022363c7c50cf5dfab732a808a3d7147" + integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== + dependencies: + mimic-function "^5.0.0" + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -25403,11 +25466,6 @@ openapi-sampler@^1.5.0: "@types/json-schema" "^7.0.7" json-pointer "0.6.2" -openapi-types@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-10.0.0.tgz#0debbf663b2feed0322030b5b7c9080804076934" - integrity sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ== - openapi-types@^12.1.3: version "12.1.3" resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" @@ -25684,12 +25742,11 @@ pac-proxy-agent@^7.0.1: socks-proxy-agent "^8.0.2" pac-resolver@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.0.tgz#79376f1ca26baf245b96b34c339d79bff25e900c" - integrity sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg== + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== dependencies: degenerator "^5.0.0" - ip "^1.1.8" netmask "^2.0.2" package-hash@^4.0.0: @@ -26001,16 +26058,21 @@ picocolors@^0.2.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pidusage@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-3.0.2.tgz#6faa5402b2530b3af2cf93d13bcf202889724a53" @@ -26811,7 +26873,7 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.9.0" function-bind "^1.1.1" -prompts@^2.0.1, prompts@^2.4.0, prompts@~2.4.2: +prompts@^2.0.1, prompts@^2.4.0, prompts@^2.4.2, prompts@~2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -27207,10 +27269,10 @@ re-resizable@^6.9.9: resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== -re2js@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.2.tgz#e344697e64d128ea65c121d6581e67ee5bfa5feb" - integrity sha512-wuv0p0BGbrVIkobV8zh82WjDurXko0QNCgaif6DdRAljgVm2iio4PVYCwjAxGaWen1/QZXWDM67dIslmz7AIbA== +re2js@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.3.tgz#1318cd0c12aa6ed3ba56d5e012311ffbfb2aef35" + integrity sha512-EuNmh7jurhHEE8Ge/lBo9JuMLb3qf866Xjjfyovw3wPc7+hlqDkZq4LwhrCQMEI+ARWfrKrHozEndzlpNT0WDg== react-clientside-effect@^1.2.6: version "1.2.6" @@ -27879,7 +27941,7 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readdir-glob@^1.0.0, readdir-glob@^1.1.2: +readdir-glob@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== @@ -27933,6 +27995,13 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -28108,6 +28177,11 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -28762,6 +28836,13 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + safe-squel@^5.12.5: version "5.12.5" resolved "https://registry.yarnpkg.com/safe-squel/-/safe-squel-5.12.5.tgz#9597cec498dc184a15fe94082b7bcc80cb4d048b" @@ -30761,7 +30842,7 @@ tar-fs@^3.0.4, tar-fs@^3.0.6: bare-fs "^2.1.1" bare-path "^2.1.0" -tar-stream@^2.1.4, tar-stream@^2.2.0: +tar-stream@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -30825,6 +30906,11 @@ tcp-port-used@^1.0.2: debug "4.3.1" is2 "^2.0.6" +teamcity-service-messages@^0.1.14: + version "0.1.14" + resolved "https://registry.yarnpkg.com/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz#193d420a5e4aef8e5e50b8c39e7865e08fbb5d8a" + integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== + teex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" @@ -31077,7 +31163,7 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@^0.2.3, tmp@~0.2.1: +tmp@^0.2.3, tmp@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== @@ -31232,7 +31318,7 @@ traverse@^0.6.6, traverse@~0.6.6: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= -tree-kill@^1.2.2: +tree-kill@1.2.2, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -31324,6 +31410,15 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths-webpack-plugin@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + tsconfig-paths@^3.14.2: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -31334,7 +31429,7 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig-paths@^4.2.0: +tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== @@ -32686,6 +32781,11 @@ watchpack@^2.2.0, watchpack@^2.4.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +watskeburt@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-4.1.0.tgz#3c0227669be646a97424b631164b1afe3d4d5344" + integrity sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw== + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -33566,15 +33666,6 @@ z-schema@^5.0.1: optionalDependencies: commander "^2.7.1" -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0" - zip-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb"