diff --git a/.github/workflows/backport-next.yml b/.github/workflows/backport-next.yml new file mode 100644 index 00000000000000..6779bb42472418 --- /dev/null +++ b/.github/workflows/backport-next.yml @@ -0,0 +1,27 @@ +on: + pull_request_target: + branches: + - main + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + runs-on: ubuntu-latest + if: | + github.event.pull_request.merged == true + && contains(github.event.pull_request.labels.*.name, 'auto-backport-next') + && ( + (github.event.action == 'labeled' && github.event.label.name == 'auto-backport-next') + || (github.event.action == 'closed') + ) + steps: + - name: Backport Action + uses: sqren/backport-github-action@v7.3.1 + with: + github_token: ${{secrets.KIBANAMACHINE_TOKEN}} + + - name: Backport log + run: cat /home/runner/.backport/backport.log diff --git a/docs/discover/document-explorer.asciidoc b/docs/discover/document-explorer.asciidoc index 99447a2478b3a3..de7b07aa4d784c 100644 --- a/docs/discover/document-explorer.asciidoc +++ b/docs/discover/document-explorer.asciidoc @@ -6,8 +6,19 @@ beta::[] *Discover* has a *Document Explorer* with resizable columns, better data sorting and comparison, and a fullscreen view. -[role="screenshot"] -image::images/document-explorer.png[Document Explorer with improved look over classic view] +++++ + + +
+++++ To use the *Document Explorer* instead of the classic document table: diff --git a/docs/discover/images/document-explorer.png b/docs/discover/images/document-explorer.png deleted file mode 100644 index 75d2826869a1d6..00000000000000 Binary files a/docs/discover/images/document-explorer.png and /dev/null differ diff --git a/package.json b/package.json index 72309e07bde772..c87ce15f455cac 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/charts": "43.1.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.1.0-canary.2", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.1.0-canary.3", "@elastic/ems-client": "8.0.0", "@elastic/eui": "48.1.1", "@elastic/filesaver": "1.1.2", @@ -218,7 +218,7 @@ "constate": "^1.3.2", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", - "core-js": "^3.21.0", + "core-js": "^3.21.1", "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -733,7 +733,7 @@ "babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-styled-components": "^2.0.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", - "backport": "7.0.1", + "backport": "^7.3.1", "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", diff --git a/packages/elastic-apm-synthtrace/BUILD.bazel b/packages/elastic-apm-synthtrace/BUILD.bazel index 7fb188de435b61..09406644f44b23 100644 --- a/packages/elastic-apm-synthtrace/BUILD.bazel +++ b/packages/elastic-apm-synthtrace/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "elastic-apm-synthtrace" PKG_REQUIRE_NAME = "@elastic/apm-synthtrace" -TYPES_PKG_REQUIRE_NAME = "@types/elastic__apm-synthtrace" SOURCE_FILES = glob( [ @@ -67,11 +66,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", validate = False, ) @@ -103,7 +100,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/elastic-apm-synthtrace/tsconfig.json b/packages/elastic-apm-synthtrace/tsconfig.json index 6ae9c20b4387ba..9d9a6ecc6b2b6c 100644 --- a/packages/elastic-apm-synthtrace/tsconfig.json +++ b/packages/elastic-apm-synthtrace/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "./src", - "sourceMap": true, - "sourceRoot": "../../../../packages/elastic-apm-synthtrace/src", "types": ["node", "jest"] }, "include": ["./src/**/*.ts"] diff --git a/packages/elastic-datemath/BUILD.bazel b/packages/elastic-datemath/BUILD.bazel index b4ed018fe9d04f..1ee7f8582cb7e6 100644 --- a/packages/elastic-datemath/BUILD.bazel +++ b/packages/elastic-datemath/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "ts_project", "pkg_npm", "p PKG_BASE_NAME = "elastic-datemath" PKG_REQUIRE_NAME = "@elastic/datemath" -TYPES_PKG_REQUIRE_NAME = "@types/elastic__datemath" SOURCE_FILES = glob([ "src/index.ts", @@ -50,10 +49,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig" ) @@ -85,7 +82,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index 02465bebe519ab..3f81ea4e68516b 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/elastic-datemath/src", "types": [ "node" ] diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index c674f9ae8e00fb..583d81ce847bdb 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-ace" PKG_REQUIRE_NAME = "@kbn/ace" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__ace" SOURCE_FILES = glob( [ @@ -68,10 +67,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -103,7 +100,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json index 4a132efa091184..0fd9a15b5dbf88 100644 --- a/packages/kbn-ace/tsconfig.json +++ b/packages/kbn-ace/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-ace/src", "stripInternal": true, "types": ["node"] }, diff --git a/packages/kbn-alerts/BUILD.bazel b/packages/kbn-alerts/BUILD.bazel index a6e5f167735c01..e6ebdaa2027012 100644 --- a/packages/kbn-alerts/BUILD.bazel +++ b/packages/kbn-alerts/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-alerts" PKG_REQUIRE_NAME = "@kbn/alerts" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__alerts" SOURCE_FILES = glob( [ @@ -74,11 +73,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -109,7 +106,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-alerts/tsconfig.json b/packages/kbn-alerts/tsconfig.json index ac523fb77a9e1a..dfe59c663d3e15 100644 --- a/packages/kbn-alerts/tsconfig.json +++ b/packages/kbn-alerts/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-alerts/src", "types": ["jest", "node"] }, "include": ["src/**/*"], diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index 94e65b2e35ba35..d144ab186a6a11 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-analytics" PKG_REQUIRE_NAME = "@kbn/analytics" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__analytics" SOURCE_FILES = glob( [ @@ -71,11 +70,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -106,7 +103,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index dfae54ad91c8e4..de4301e2a2ac05 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "isolatedModules": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-analytics/src", "stripInternal": true, "types": [ "node" diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index a18a5e973d3a0f..bcdbefb132aa6d 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-apm-config-loader" PKG_REQUIRE_NAME = "@kbn/apm-config-loader" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__apm-config-loader" SOURCE_FILES = glob( [ @@ -66,10 +65,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -101,7 +98,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index 2f6da800d9dd20..7d2597d318b310 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "./src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-apm-config-loader/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-apm-utils/BUILD.bazel b/packages/kbn-apm-utils/BUILD.bazel index c8ad4d1a09625f..9ca9009bb7186b 100644 --- a/packages/kbn-apm-utils/BUILD.bazel +++ b/packages/kbn-apm-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-apm-utils" PKG_REQUIRE_NAME = "@kbn/apm-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__apm-utils" SOURCE_FILES = glob([ "src/index.ts", @@ -51,10 +50,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -86,7 +83,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-apm-utils/tsconfig.json b/packages/kbn-apm-utils/tsconfig.json index d7056cda6d71ab..9c8c443436ce5c 100644 --- a/packages/kbn-apm-utils/tsconfig.json +++ b/packages/kbn-apm-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-apm-utils/src", "types": [ "node" ] diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 133474a3aefa61..4b45e34b7e9fa3 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-cli-dev-mode" PKG_REQUIRE_NAME = "@kbn/cli-dev-mode" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__cli-dev-mode" SOURCE_FILES = glob( [ @@ -93,11 +92,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -128,7 +125,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index 6ce2e21b846741..4ba84d786cb4b1 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "./src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-cli-dev-mode/src", "types": [ "jest", "node" diff --git a/packages/kbn-config-schema/BUILD.bazel b/packages/kbn-config-schema/BUILD.bazel index ed6082527bab92..e496aa31b49d33 100644 --- a/packages/kbn-config-schema/BUILD.bazel +++ b/packages/kbn-config-schema/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-config-schema" PKG_REQUIRE_NAME = "@kbn/config-schema" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__config-schema" SOURCE_FILES = glob([ "src/**/*.ts", @@ -62,10 +61,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -97,7 +94,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index 79b652daf7ec1d..e1125366bc3900 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-config-schema/src", "stripInternal": true, "types": [ "jest", diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index 0577014768d4ca..e242cf5c926229 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-config" PKG_REQUIRE_NAME = "@kbn/config" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__config" SOURCE_FILES = glob( [ @@ -83,10 +82,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -118,7 +115,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-config/tsconfig.json b/packages/kbn-config/tsconfig.json index 0971923a11a0f6..a684de9502b5e5 100644 --- a/packages/kbn-config/tsconfig.json +++ b/packages/kbn-config/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-config/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index f71c8b866fd5d9..de8c97ed3b713a 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__crypto" SOURCE_FILES = glob( [ @@ -29,7 +28,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:build", "@npm//node-forge", ] @@ -62,10 +61,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -97,7 +94,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index 0863fc3f530def..272363e976ba1e 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-crypto/src", "types": [ "jest", "node" diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 65e0957fe5d90f..7be527c65a06c4 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-dev-utils" PKG_REQUIRE_NAME = "@kbn/dev-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__dev-utils" SOURCE_FILES = glob( [ @@ -114,10 +113,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -149,7 +146,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 3c22642edfaa1e..a8cfc2cceb08b8 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-dev-utils/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-doc-links/BUILD.bazel b/packages/kbn-doc-links/BUILD.bazel index b4ae92aa8050b5..13b68935c43261 100644 --- a/packages/kbn-doc-links/BUILD.bazel +++ b/packages/kbn-doc-links/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-doc-links" PKG_REQUIRE_NAME = "@kbn/doc-links" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__doc-links" SOURCE_FILES = glob( [ @@ -61,10 +60,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -96,7 +93,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 5f4c07ee6067ce..d73760b280d496 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -544,6 +544,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { lowercase: `${ELASTICSEARCH_DOCS}lowercase-processor.html`, pipeline: `${ELASTICSEARCH_DOCS}pipeline-processor.html`, pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, + csvPipelines: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${DOC_LINK_VERSION}/ecs-converting.html`, pipelineFailure: `${ELASTICSEARCH_DOCS}ingest.html#handling-pipeline-failures`, processors: `${ELASTICSEARCH_DOCS}processors.html`, remove: `${ELASTICSEARCH_DOCS}remove-processor.html`, diff --git a/packages/kbn-doc-links/tsconfig.json b/packages/kbn-doc-links/tsconfig.json index e2178f72072e98..a684de9502b5e5 100644 --- a/packages/kbn-doc-links/tsconfig.json +++ b/packages/kbn-doc-links/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-doc-links/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index ad6b6687b7e1af..30baa9dccb7f78 100644 --- a/packages/kbn-docs-utils/BUILD.bazel +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-docs-utils" PKG_REQUIRE_NAME = "@kbn/docs-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__docs-utils" SOURCE_FILES = glob( [ @@ -67,10 +66,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -102,7 +99,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index fa20d6f4be398e..9a68b2e81940cf 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-docs-utils/src", "types": [ "jest", "node" diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index fed3f248c09957..70bda4c67106f2 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-es-archiver" PKG_REQUIRE_NAME = "@kbn/es-archiver" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__es-archiver" SOURCE_FILES = glob( [ @@ -80,10 +79,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -115,7 +112,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 15c846f052b473..92bc23ab6616b8 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-es-archiver/src", "types": [ "jest", "node" diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 84ee5584ceabe7..c7d8ad33cf7f66 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-es-query" PKG_REQUIRE_NAME = "@kbn/es-query" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__es-query" SOURCE_FILES = glob( [ @@ -94,10 +93,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -129,7 +126,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index 5b1f3be263138b..4b2faade0d50a8 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-es-query/src", "types": [ "jest", "node" diff --git a/packages/kbn-es/src/cli_commands/archive.js b/packages/kbn-es/src/cli_commands/archive.js index c92ed98ce03fc8..96ffc1fec34c20 100644 --- a/packages/kbn-es/src/cli_commands/archive.js +++ b/packages/kbn-es/src/cli_commands/archive.js @@ -10,6 +10,7 @@ const dedent = require('dedent'); const getopts = require('getopts'); const { Cluster } = require('../cluster'); const { createCliError } = require('../errors'); +const { parseTimeoutToMs } = require('../utils'); exports.description = 'Install and run from an Elasticsearch tar'; @@ -27,6 +28,8 @@ exports.help = (defaults = {}) => { --password.[user] Sets password for native realm user [default: ${password}] --ssl Sets up SSL on Elasticsearch -E Additional key=value settings to pass to Elasticsearch + --skip-ready-check Disable the ready check, + --ready-timeout Customize the ready check timeout, in seconds or "Xm" format, defaults to 1m Example: @@ -41,8 +44,13 @@ exports.run = async (defaults = {}) => { basePath: 'base-path', installPath: 'install-path', esArgs: 'E', + skipReadyCheck: 'skip-ready-check', + readyTimeout: 'ready-timeout', }, + string: ['ready-timeout'], + boolean: ['skip-ready-check'], + default: defaults, }); @@ -54,5 +62,8 @@ exports.run = async (defaults = {}) => { } const { installPath } = await cluster.installArchive(path, options); - await cluster.run(installPath, options); + await cluster.run(installPath, { + ...options, + readyTimeout: parseTimeoutToMs(options.readyTimeout), + }); }; diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js index b89f1f82148138..1c902796a0a0c9 100644 --- a/packages/kbn-es/src/cli_commands/snapshot.js +++ b/packages/kbn-es/src/cli_commands/snapshot.js @@ -10,6 +10,7 @@ const dedent = require('dedent'); const getopts = require('getopts'); import { ToolingLog, getTimeReporter } from '@kbn/dev-utils'; const { Cluster } = require('../cluster'); +const { parseTimeoutToMs } = require('../utils'); exports.description = 'Downloads and run from a nightly snapshot'; @@ -30,6 +31,8 @@ exports.help = (defaults = {}) => { --download-only Download the snapshot but don't actually start it --ssl Sets up SSL on Elasticsearch --use-cached Skips cache verification and use cached ES snapshot. + --skip-ready-check Disable the ready check, + --ready-timeout Customize the ready check timeout, in seconds or "Xm" format, defaults to 1m Example: @@ -53,11 +56,12 @@ exports.run = async (defaults = {}) => { dataArchive: 'data-archive', esArgs: 'E', useCached: 'use-cached', + skipReadyCheck: 'skip-ready-check', + readyTimeout: 'ready-timeout', }, - string: ['version'], - - boolean: ['download-only', 'use-cached'], + string: ['version', 'ready-timeout'], + boolean: ['download-only', 'use-cached', 'skip-ready-check'], default: defaults, }); @@ -82,6 +86,7 @@ exports.run = async (defaults = {}) => { reportTime, startTime: runStartTime, ...options, + readyTimeout: parseTimeoutToMs(options.readyTimeout), }); } }; diff --git a/packages/kbn-es/src/cli_commands/source.js b/packages/kbn-es/src/cli_commands/source.js index 5a4192ae7703cb..c16e89e2c7f32a 100644 --- a/packages/kbn-es/src/cli_commands/source.js +++ b/packages/kbn-es/src/cli_commands/source.js @@ -9,6 +9,7 @@ const dedent = require('dedent'); const getopts = require('getopts'); const { Cluster } = require('../cluster'); +const { parseTimeoutToMs } = require('../utils'); exports.description = 'Build and run from source'; @@ -27,6 +28,8 @@ exports.help = (defaults = {}) => { --password.[user] Sets password for native realm user [default: ${password}] --ssl Sets up SSL on Elasticsearch -E Additional key=value settings to pass to Elasticsearch + --skip-ready-check Disable the ready check, + --ready-timeout Customize the ready check timeout, in seconds or "Xm" format, defaults to 1m Example: @@ -42,9 +45,14 @@ exports.run = async (defaults = {}) => { installPath: 'install-path', sourcePath: 'source-path', dataArchive: 'data-archive', + skipReadyCheck: 'skip-ready-check', + readyTimeout: 'ready-timeout', esArgs: 'E', }, + string: ['ready-timeout'], + boolean: ['skip-ready-check'], + default: defaults, }); @@ -55,5 +63,8 @@ exports.run = async (defaults = {}) => { await cluster.extractDataDirectory(installPath, options.dataArchive); } - await cluster.run(installPath, options); + await cluster.run(installPath, { + ...options, + readyTimeout: parseTimeoutToMs(options.readyTimeout), + }); }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 630a5e65678873..4ad73297523474 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -6,20 +6,29 @@ * Side Public License, v 1. */ -const fs = require('fs'); -const util = require('util'); +const fsp = require('fs/promises'); const execa = require('execa'); const chalk = require('chalk'); const path = require('path'); +const { Client } = require('@elastic/elasticsearch'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { ES_BIN } = require('./paths'); -const { log: defaultLog, parseEsLog, extractConfigFiles, NativeRealm } = require('./utils'); +const { + log: defaultLog, + parseEsLog, + extractConfigFiles, + NativeRealm, + parseTimeoutToMs, +} = require('./utils'); const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); const { parseSettings, SettingsFilter } = require('./settings'); const { CA_CERT_PATH, ES_NOPASSWORD_P12_PATH, extract } = require('@kbn/dev-utils'); -const readFile = util.promisify(fs.readFile); + +const DEFAULT_READY_TIMEOUT = parseTimeoutToMs('1m'); + +/** @typedef {import('./cluster_exec_options').EsClusterExecOptions} ExecOptions */ // listen to data on stream until map returns anything but undefined const first = (stream, map) => @@ -38,7 +47,6 @@ exports.Cluster = class Cluster { constructor({ log = defaultLog, ssl = false } = {}) { this._log = log.withType('@kbn/es Cluster'); this._ssl = ssl; - this._caCertPromise = ssl ? readFile(CA_CERT_PATH) : undefined; } /** @@ -157,10 +165,8 @@ exports.Cluster = class Cluster { * Starts ES and returns resolved promise once started * * @param {String} installPath - * @param {Object} options - * @property {Array} options.esArgs - * @property {String} options.password - super user password used to bootstrap - * @returns {Promise} + * @param {ExecOptions} options + * @returns {Promise} */ async start(installPath, options = {}) { this._exec(installPath, options); @@ -173,7 +179,7 @@ exports.Cluster = class Cluster { return true; } }), - this._nativeRealmSetup, + this._setupPromise, ]), // await the outcome of the process in case it exits before starting @@ -187,15 +193,14 @@ exports.Cluster = class Cluster { * Starts Elasticsearch and waits for Elasticsearch to exit * * @param {String} installPath - * @param {Object} options - * @property {Array} options.esArgs - * @returns {Promise} + * @param {ExecOptions} options + * @returns {Promise} */ async run(installPath, options = {}) { this._exec(installPath, options); // log native realm setup errors so they aren't uncaught - this._nativeRealmSetup.catch((error) => { + this._setupPromise.catch((error) => { this._log.error(error); this.stop(); }); @@ -233,14 +238,17 @@ exports.Cluster = class Cluster { * * @private * @param {String} installPath - * @param {Object} options - * @property {string|Array} options.esArgs - * @property {string} options.esJavaOpts - * @property {Boolean} options.skipNativeRealmSetup - * @return {undefined} + * @param {ExecOptions} opts */ _exec(installPath, opts = {}) { - const { skipNativeRealmSetup = false, reportTime = () => {}, startTime, ...options } = opts; + const { + skipNativeRealmSetup = false, + reportTime = () => {}, + startTime, + skipReadyCheck, + readyTimeout, + ...options + } = opts; if (this._process || this._outcome) { throw new Error('ES has already been started'); @@ -252,6 +260,7 @@ exports.Cluster = class Cluster { const esArgs = [ 'action.destructive_requires_name=true', 'ingest.geoip.downloader.enabled=false', + 'search.check_ccs_compatibility=true', ].concat(options.esArgs || []); // Add to esArgs if ssl is enabled @@ -274,7 +283,7 @@ exports.Cluster = class Cluster { [] ); - this._log.debug('%s %s', ES_BIN, args.join(' ')); + this._log.info('%s %s', ES_BIN, args.join(' ')); let esJavaOpts = `${options.esJavaOpts || ''} ${process.env.ES_JAVA_OPTS || ''}`; @@ -287,7 +296,7 @@ exports.Cluster = class Cluster { esJavaOpts += ' -Xms1536m -Xmx1536m'; } - this._log.debug('ES_JAVA_OPTS: %s', esJavaOpts.trim()); + this._log.info('ES_JAVA_OPTS: %s', esJavaOpts.trim()); this._process = execa(ES_BIN, args, { cwd: installPath, @@ -300,30 +309,49 @@ exports.Cluster = class Cluster { stdio: ['ignore', 'pipe', 'pipe'], }); - // parse log output to find http port - const httpPort = first(this._process.stdout, (data) => { - const match = data.toString('utf8').match(/HttpServer.+publish_address {[0-9.]+:([0-9]+)/); + this._setupPromise = Promise.all([ + // parse log output to find http port + first(this._process.stdout, (data) => { + const match = data.toString('utf8').match(/HttpServer.+publish_address {[0-9.]+:([0-9]+)/); - if (match) { - return match[1]; + if (match) { + return match[1]; + } + }), + + // load the CA cert from disk if necessary + this._ssl ? fsp.readFile(CA_CERT_PATH) : null, + ]).then(async ([port, caCert]) => { + const client = new Client({ + node: `${caCert ? 'https:' : 'http:'}//localhost:${port}`, + auth: { + username: 'elastic', + password: options.password, + }, + tls: caCert + ? { + ca: caCert, + rejectUnauthorized: true, + } + : undefined, + }); + + if (!skipReadyCheck) { + await this._waitForClusterReady(client, readyTimeout); } - }); - // once the http port is available setup the native realm - this._nativeRealmSetup = httpPort.then(async (port) => { - if (skipNativeRealmSetup) { - return; + // once the cluster is ready setup the native realm + if (!skipNativeRealmSetup) { + const nativeRealm = new NativeRealm({ + log: this._log, + elasticPassword: options.password, + client, + }); + + await nativeRealm.setPasswords(options); } - const caCert = await this._caCertPromise; - const nativeRealm = new NativeRealm({ - port, - caCert, - log: this._log, - elasticPassword: options.password, - ssl: this._ssl, - }); - await nativeRealm.setPasswords(options); + this._log.success('kbn/es setup complete'); }); let reportSent = false; @@ -366,4 +394,43 @@ exports.Cluster = class Cluster { } }); } + + async _waitForClusterReady(client, readyTimeout = DEFAULT_READY_TIMEOUT) { + let attempt = 0; + const start = Date.now(); + + this._log.info('waiting for ES cluster to report a yellow or green status'); + + while (true) { + attempt += 1; + + try { + const resp = await client.cluster.health(); + if (resp.status !== 'red') { + return; + } + + throw new Error(`not ready, cluster health is ${resp.status}`); + } catch (error) { + const timeSinceStart = Date.now() - start; + if (timeSinceStart > readyTimeout) { + const sec = readyTimeout / 1000; + throw new Error(`ES cluster failed to come online with the ${sec} second timeout`); + } + + if (error.message.startsWith('not ready,')) { + if (timeSinceStart > 10_000) { + this._log.warning(error.message); + } + } else { + this._log.warning( + `waiting for ES cluster to come online, attempt ${attempt} failed with: ${error.message}` + ); + } + + const waitSec = attempt * 1.5; + await new Promise((resolve) => setTimeout(resolve, waitSec * 1000)); + } + } + } }; diff --git a/packages/kbn-es/src/cluster_exec_options.ts b/packages/kbn-es/src/cluster_exec_options.ts new file mode 100644 index 00000000000000..8ef3b23cd8c51d --- /dev/null +++ b/packages/kbn-es/src/cluster_exec_options.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface EsClusterExecOptions { + skipNativeRealmSetup?: boolean; + reportTime?: (...args: any[]) => void; + startTime?: number; + esArgs?: string[]; + esJavaOpts?: string; + password?: string; + skipReadyCheck?: boolean; + readyTimeout?: number; +} diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index c4b15123314838..5633bd32b9ab22 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -309,6 +309,7 @@ describe('#start(installPath)', () => { Array [ "action.destructive_requires_name=true", "ingest.geoip.downloader.enabled=false", + "search.check_ccs_compatibility=true", ], undefined, Object { @@ -387,6 +388,7 @@ describe('#run()', () => { Array [ "action.destructive_requires_name=true", "ingest.geoip.downloader.enabled=false", + "search.check_ccs_compatibility=true", ], undefined, Object { diff --git a/packages/kbn-es/src/utils/index.ts b/packages/kbn-es/src/utils/index.ts index 4b4ae1bc05259f..4e75d1d81f6fb3 100644 --- a/packages/kbn-es/src/utils/index.ts +++ b/packages/kbn-es/src/utils/index.ts @@ -17,3 +17,4 @@ export { extractConfigFiles } from './extract_config_files'; export { NativeRealm, SYSTEM_INDICES_SUPERUSER } from './native_realm'; export { buildSnapshot } from './build_snapshot'; export { archiveForPlatform } from './build_snapshot'; +export * from './parse_timeout_to_ms'; diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index 52d6ae807777bf..ae0ce05f4d6b72 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -const { Client } = require('@elastic/elasticsearch'); const chalk = require('chalk'); const { log: defaultLog } = require('./log'); @@ -15,14 +14,9 @@ export const SYSTEM_INDICES_SUPERUSER = process.env.TEST_ES_SYSTEM_INDICES_USER || 'system_indices_superuser'; exports.NativeRealm = class NativeRealm { - constructor({ elasticPassword, port, log = defaultLog, ssl = false, caCert }) { - const auth = { username: 'elastic', password: elasticPassword }; - this._client = new Client( - ssl - ? { node: `https://localhost:${port}`, tls: { ca: caCert, rejectUnauthorized: true }, auth } - : { node: `http://localhost:${port}`, auth } - ); + constructor({ elasticPassword, log = defaultLog, client }) { this._elasticPassword = elasticPassword; + this._client = client; this._log = log; } @@ -53,24 +47,14 @@ exports.NativeRealm = class NativeRealm { }); } - async clusterReady() { - return await this._autoRetry({ maxAttempts: 10 }, async () => { - const { status } = await this._client.cluster.health(); - if (status === 'red') { - throw new Error(`not ready, cluster health is ${status}`); - } - }); - } - async setPasswords(options) { - await this.clusterReady(); - if (!(await this.isSecurityEnabled())) { this._log.info('security is not enabled, unable to set native realm passwords'); return; } const reservedUsers = await this.getReservedUsers(); + this._log.info(`Set up ${reservedUsers.length} ES users`); await Promise.all([ ...reservedUsers.map(async (user) => { await this.setPassword(user, options[`password.${user}`]); @@ -108,7 +92,7 @@ exports.NativeRealm = class NativeRealm { } async _autoRetry(opts, fn) { - const { attempt = 1, maxAttempts = 3, sleep = 1000 } = opts; + const { attempt = 1, maxAttempts = 3 } = opts; try { return await fn(attempt); @@ -119,7 +103,7 @@ exports.NativeRealm = class NativeRealm { const sec = 1.5 * attempt; this._log.warning(`assuming ES isn't initialized completely, trying again in ${sec} seconds`); - await new Promise((resolve) => setTimeout(resolve, sleep)); + await new Promise((resolve) => setTimeout(resolve, sec * 1000)); const nextOpts = { ...opts, diff --git a/packages/kbn-es/src/utils/native_realm.test.js b/packages/kbn-es/src/utils/native_realm.test.js index a567c15e743aff..d3eaf6bd97b72d 100644 --- a/packages/kbn-es/src/utils/native_realm.test.js +++ b/packages/kbn-es/src/utils/native_realm.test.js @@ -7,12 +7,7 @@ */ const { NativeRealm } = require('./native_realm'); - -jest.genMockFromModule('@elastic/elasticsearch'); -jest.mock('@elastic/elasticsearch'); - const { ToolingLog } = require('@kbn/dev-utils'); -const { Client } = require('@elastic/elasticsearch'); const mockClient = { xpack: { @@ -28,13 +23,12 @@ const mockClient = { putUser: jest.fn(), }, }; -Client.mockImplementation(() => mockClient); const log = new ToolingLog(); let nativeRealm; beforeEach(() => { - nativeRealm = new NativeRealm({ elasticPassword: 'changeme', port: '9200', log }); + nativeRealm = new NativeRealm({ elasticPassword: 'changeme', client: mockClient, log }); }); afterAll(() => { diff --git a/packages/kbn-es/src/utils/parse_timeout_to_ms.test.ts b/packages/kbn-es/src/utils/parse_timeout_to_ms.test.ts new file mode 100644 index 00000000000000..fba387cad278bf --- /dev/null +++ b/packages/kbn-es/src/utils/parse_timeout_to_ms.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseTimeoutToMs } from './parse_timeout_to_ms'; + +it('handles empty values', () => { + expect(parseTimeoutToMs(undefined)).toMatchInlineSnapshot(`undefined`); + expect(parseTimeoutToMs('')).toMatchInlineSnapshot(`undefined`); +}); +it('returns numbers', () => { + expect(parseTimeoutToMs(10)).toMatchInlineSnapshot(`10`); +}); +it('parses seconds', () => { + expect(parseTimeoutToMs('10')).toMatchInlineSnapshot(`10000`); +}); +it('parses minutes', () => { + expect(parseTimeoutToMs('10m')).toMatchInlineSnapshot(`600000`); +}); +it('throws for invalid values', () => { + expect(() => parseTimeoutToMs(true)).toThrowErrorMatchingInlineSnapshot( + `"[true] is not a valid timeout value"` + ); + expect(() => parseTimeoutToMs([true])).toThrowErrorMatchingInlineSnapshot( + `"[[ true ]] is not a valid timeout value"` + ); + expect(() => parseTimeoutToMs(['true'])).toThrowErrorMatchingInlineSnapshot( + `"[[ 'true' ]] is not a valid timeout value"` + ); + expect(() => parseTimeoutToMs(NaN)).toThrowErrorMatchingInlineSnapshot( + `"[NaN] is not a valid timeout value"` + ); +}); diff --git a/packages/kbn-es/src/utils/parse_timeout_to_ms.ts b/packages/kbn-es/src/utils/parse_timeout_to_ms.ts new file mode 100644 index 00000000000000..c8272bdfeee512 --- /dev/null +++ b/packages/kbn-es/src/utils/parse_timeout_to_ms.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { inspect } from 'util'; + +function parseInt(n: string) { + const number = Number.parseInt(n, 10); + if (Number.isNaN(number)) { + throw new Error(`invalid number [${n}]`); + } + return number; +} + +/** + * Parse a timeout value to milliseconds. Supports undefined, a number, an + * empty string, a string representing a number of minutes eg 1m, or a string + * representing a number of seconds eg 60. All other values throw an error + */ +export function parseTimeoutToMs(seconds: any): number | undefined { + if (seconds === undefined || seconds === '') { + return undefined; + } + + if (typeof seconds === 'number' && !Number.isNaN(seconds)) { + return seconds; + } + + if (typeof seconds !== 'string') { + throw new Error(`[${inspect(seconds)}] is not a valid timeout value`); + } + + if (seconds.endsWith('m')) { + const m = parseInt(seconds.slice(0, -1)); + return m * 60 * 1000; + } + + return parseInt(seconds) * 1000; +} diff --git a/packages/kbn-field-types/BUILD.bazel b/packages/kbn-field-types/BUILD.bazel index 0398d4d9b9ba62..77a4acaedb2359 100644 --- a/packages/kbn-field-types/BUILD.bazel +++ b/packages/kbn-field-types/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-field-types" PKG_REQUIRE_NAME = "@kbn/field-types" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__field-types" SOURCE_FILES = glob( [ @@ -64,10 +63,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -99,7 +96,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-field-types/tsconfig.json b/packages/kbn-field-types/tsconfig.json index 150854076bbf82..f4dd1662f832f7 100644 --- a/packages/kbn-field-types/tsconfig.json +++ b/packages/kbn-field-types/tsconfig.json @@ -3,11 +3,8 @@ "compilerOptions": { "outDir": "./target_types", "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-field-types/src" }, "include": ["src/**/*"] } diff --git a/packages/kbn-i18n-react/BUILD.bazel b/packages/kbn-i18n-react/BUILD.bazel index 505afabfa860d7..0ba73353903804 100644 --- a/packages/kbn-i18n-react/BUILD.bazel +++ b/packages/kbn-i18n-react/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-i18n-react" PKG_REQUIRE_NAME = "@kbn/i18n-react" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__i18n-react" SOURCE_FILES = glob( [ @@ -74,10 +73,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -109,7 +106,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-i18n-react/tsconfig.json b/packages/kbn-i18n-react/tsconfig.json index d2707938041d2a..5f0c08bace6d33 100644 --- a/packages/kbn-i18n-react/tsconfig.json +++ b/packages/kbn-i18n-react/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-i18n-react/src", "types": [ "jest", "node" diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel index 385bdafb7c8ee4..07b2c0b4d773ae 100644 --- a/packages/kbn-i18n/BUILD.bazel +++ b/packages/kbn-i18n/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-i18n" PKG_REQUIRE_NAME = "@kbn/i18n" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__i18n" SOURCE_FILES = glob( [ @@ -75,10 +74,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -110,7 +107,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 2ac0b081b76ddb..0fd24d62281b6a 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-i18n/src", "types": [ "jest", "node" diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index fd19413116f8d5..d390ccbc2ebde7 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-interpreter" PKG_REQUIRE_NAME = "@kbn/interpreter" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__interpreter" SOURCE_FILES = glob( [ @@ -74,10 +73,8 @@ ts_project( deps = TYPES_DEPS, allow_js = True, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -109,7 +106,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 60f8c76cf8809b..de30741c59a60c 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -3,12 +3,9 @@ "compilerOptions": { "allowJs": true, "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-interpreter/src", "stripInternal": true, "types": [ "jest", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 5ecfc0acc55e8e..aa0116b81efe68 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-io-ts-utils" PKG_REQUIRE_NAME = "@kbn/io-ts-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__io-ts-utils" SOURCE_FILES = glob( [ @@ -65,10 +64,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -100,7 +97,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json index 72d14796213455..1998f132ffd246 100644 --- a/packages/kbn-io-ts-utils/tsconfig.json +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-io-ts-utils/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-logging-mocks/BUILD.bazel b/packages/kbn-logging-mocks/BUILD.bazel index 74fb9c2651e5d3..90a20464442914 100644 --- a/packages/kbn-logging-mocks/BUILD.bazel +++ b/packages/kbn-logging-mocks/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-logging-mocks" PKG_REQUIRE_NAME = "@kbn/logging-mocks" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging-mocks" SOURCE_FILES = glob( [ @@ -57,10 +56,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -92,7 +89,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-logging-mocks/tsconfig.json b/packages/kbn-logging-mocks/tsconfig.json index ce53e016c2830e..ee25c507e7f562 100644 --- a/packages/kbn-logging-mocks/tsconfig.json +++ b/packages/kbn-logging-mocks/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-logging-mocks/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-logging/BUILD.bazel b/packages/kbn-logging/BUILD.bazel index 09ff3f0d83b2de..ec25b5cd3ae88c 100644 --- a/packages/kbn-logging/BUILD.bazel +++ b/packages/kbn-logging/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-logging" PKG_REQUIRE_NAME = "@kbn/logging" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging" SOURCE_FILES = glob( [ @@ -58,10 +57,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -93,7 +90,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-logging/tsconfig.json b/packages/kbn-logging/tsconfig.json index a6fb0f2f731871..ee25c507e7f562 100644 --- a/packages/kbn-logging/tsconfig.json +++ b/packages/kbn-logging/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-logging/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index b09b783a0aebf3..89cbeb4c431ae0 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-mapbox-gl" PKG_REQUIRE_NAME = "@kbn/mapbox-gl" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__mapbox-gl" SOURCE_FILES = glob( [ @@ -61,10 +60,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -96,7 +93,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json index e935276e917623..ee063bd30933e7 100644 --- a/packages/kbn-mapbox-gl/tsconfig.json +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-mapbox-gl/src", "types": [] }, "include": [ diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index c72c46f8ed2020..c615196f5c1823 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-monaco" PKG_REQUIRE_NAME = "@kbn/monaco" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__monaco" SOURCE_FILES = glob( [ @@ -96,10 +95,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -131,7 +128,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 959051b17b782b..e55d786c41bc33 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-monaco/src", "types": [ "jest", "node" diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index 9486f309bd0f3d..680ca0e58b8eb2 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-optimizer" PKG_REQUIRE_NAME = "@kbn/optimizer" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__optimizer" SOURCE_FILES = glob( [ @@ -119,10 +118,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -154,7 +151,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f786d012322270..79308c43bf0aa9 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -100,7 +100,7 @@ pageLoadAssetSize: bfetch: 22837 kibanaUtils: 79713 data: 491273 - dataViews: 41532 + dataViews: 42532 expressions: 140958 fieldFormats: 65209 kibanaReact: 74422 diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index 5fbd02106e777e..72de52e593cf72 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "./src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-optimizer/src", "types": [ "jest", "node" diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index 0578842a7509ba..3e1565e86fe0d9 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-plugin-generator" PKG_REQUIRE_NAME = "@kbn/plugin-generator" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__plugin-generator" SOURCE_FILES = glob( [ @@ -86,10 +85,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -121,7 +118,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index 5b666cf801da63..519de9b703140c 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-plugin-generator/src", "target": "ES2019", "types": [ "jest", diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel index 7112c497f6ff8c..cda9dce927e357 100644 --- a/packages/kbn-plugin-helpers/BUILD.bazel +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-plugin-helpers" PKG_REQUIRE_NAME = "@kbn/plugin-helpers" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__plugin-helpers" SOURCE_FILES = glob( [ @@ -79,10 +78,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -114,7 +111,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index 34f3ec5e675038..0bff4cdbb85e1d 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-plugin-helpers/src", "target": "ES2018", "types": [ "jest", diff --git a/packages/kbn-react-field/BUILD.bazel b/packages/kbn-react-field/BUILD.bazel index 36ab9d7f38c560..07ecbcb61db26f 100644 --- a/packages/kbn-react-field/BUILD.bazel +++ b/packages/kbn-react-field/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-react-field" PKG_REQUIRE_NAME = "@kbn/react-field" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__react-field" SOURCE_FILES = glob( [ @@ -84,10 +83,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -119,7 +116,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-react-field/tsconfig.json b/packages/kbn-react-field/tsconfig.json index 4d37e1825c85a5..f91c1f4c54d747 100644 --- a/packages/kbn-react-field/tsconfig.json +++ b/packages/kbn-react-field/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-react-field/src", "types": [ "jest", "node", diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 1e71947566722b..6477b558db9cb3 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-rule-data-utils" PKG_REQUIRE_NAME = "@kbn/rule-data-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__rule-data-utils" SOURCE_FILES = glob( [ @@ -60,11 +59,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, incremental = False, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -96,7 +93,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json index be6095d187ef31..05947e9286a740 100644 --- a/packages/kbn-rule-data-utils/tsconfig.json +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -2,13 +2,10 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "incremental": false, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index bc29f400f4d5b2..16aa28e79b45be 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-autocomplete" PKG_REQUIRE_NAME = "@kbn/securitysolution-autocomplete" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-autocomplete" SOURCE_FILES = glob( [ @@ -93,11 +92,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -128,7 +125,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json index b2e24676cdbd41..dfe59c663d3e15 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-autocomplete/src", "rootDir": "src", "types": ["jest", "node"] }, diff --git a/packages/kbn-securitysolution-es-utils/BUILD.bazel b/packages/kbn-securitysolution-es-utils/BUILD.bazel index ceb643246249ea..e36409f464b3e0 100644 --- a/packages/kbn-securitysolution-es-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-es-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-es-utils" PKG_REQUIRE_NAME = "@kbn/securitysolution-es-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-es-utils" SOURCE_FILES = glob( [ @@ -65,11 +64,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -100,7 +97,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-es-utils/tsconfig.json b/packages/kbn-securitysolution-es-utils/tsconfig.json index e5b1c99d3f598c..305954d669b758 100644 --- a/packages/kbn-securitysolution-es-utils/tsconfig.json +++ b/packages/kbn-securitysolution-es-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-es-utils/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-hook-utils/BUILD.bazel b/packages/kbn-securitysolution-hook-utils/BUILD.bazel index 4f46992ad13d6f..363d4f688783da 100644 --- a/packages/kbn-securitysolution-hook-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-hook-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-hook-utils" PKG_REQUIRE_NAME = "@kbn/securitysolution-hook-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-hook-utils" SOURCE_FILES = glob( [ @@ -72,11 +71,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -107,7 +104,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-hook-utils/tsconfig.json b/packages/kbn-securitysolution-hook-utils/tsconfig.json index 4782331e31c440..6a1981335d68ea 100644 --- a/packages/kbn-securitysolution-hook-utils/tsconfig.json +++ b/packages/kbn-securitysolution-hook-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-hook-utils/src", "types": ["jest", "node"] }, "include": ["src/**/*"] diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel index 61cdb9cccd2929..0a06de87e83038 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-io-ts-alerting-types" PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-alerting-types" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-io-ts-alerting-types" SOURCE_FILES = glob( [ @@ -75,11 +74,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -110,7 +107,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json index 0e58572c1b82bf..305954d669b758 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-alerting-types/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel index 66e52c1f509b44..d4ba51713d7e34 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-io-ts-list-types" PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-list-types" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-io-ts-list-types" SOURCE_FILES = glob( [ @@ -75,11 +74,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -110,7 +107,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json index ca0ea969f89d8a..305954d669b758 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-list-types/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel index a4f817ac55f092..794eab1635b73d 100644 --- a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-io-ts-types" PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-types" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-io-ts-types" SOURCE_FILES = glob( [ @@ -73,11 +72,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -108,7 +105,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json index c640181145be84..305954d669b758 100644 --- a/packages/kbn-securitysolution-io-ts-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-types/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel index 46afff1a6affbd..0229acdb474e4f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-io-ts-utils" PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-io-ts-utils" SOURCE_FILES = glob( [ @@ -76,11 +75,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -111,7 +108,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json index 7c71143083d4d4..305954d669b758 100644 --- a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-utils/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-list-api/BUILD.bazel b/packages/kbn-securitysolution-list-api/BUILD.bazel index 587b564ab91926..c73b9b4ea7503c 100644 --- a/packages/kbn-securitysolution-list-api/BUILD.bazel +++ b/packages/kbn-securitysolution-list-api/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-list-api" PKG_REQUIRE_NAME = "@kbn/securitysolution-list-api" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-list-api" SOURCE_FILES = glob( [ @@ -75,11 +74,9 @@ ts_project( deps = TYPES_DEPS, args = ["--pretty"], declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -110,7 +107,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-list-api/tsconfig.json b/packages/kbn-securitysolution-list-api/tsconfig.json index d51cd3ac71d8d9..305954d669b758 100644 --- a/packages/kbn-securitysolution-list-api/tsconfig.json +++ b/packages/kbn-securitysolution-list-api/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-list-api/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-list-constants/BUILD.bazel b/packages/kbn-securitysolution-list-constants/BUILD.bazel index 3802e3a5819697..339743721bc84b 100644 --- a/packages/kbn-securitysolution-list-constants/BUILD.bazel +++ b/packages/kbn-securitysolution-list-constants/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-list-constants" PKG_REQUIRE_NAME = "@kbn/securitysolution-list-constants" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-list-constants" SOURCE_FILES = glob( [ @@ -63,11 +62,9 @@ ts_project( deps = TYPES_DEPS, args = ["--pretty"], declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -98,7 +95,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-list-constants/tsconfig.json b/packages/kbn-securitysolution-list-constants/tsconfig.json index 8697cbd61580a8..305954d669b758 100644 --- a/packages/kbn-securitysolution-list-constants/tsconfig.json +++ b/packages/kbn-securitysolution-list-constants/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-list-constants/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index 7b3fc87b6f87e1..d709a0ddfbb80e 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-list-hooks" PKG_REQUIRE_NAME = "@kbn/securitysolution-list-hooks" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-list-hooks" SOURCE_FILES = glob( [ @@ -83,11 +82,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -118,7 +115,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-list-hooks/tsconfig.json b/packages/kbn-securitysolution-list-hooks/tsconfig.json index 41ec03f2ebf352..305954d669b758 100644 --- a/packages/kbn-securitysolution-list-hooks/tsconfig.json +++ b/packages/kbn-securitysolution-list-hooks/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-list-hooks/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index 87e546a1fff089..642e4a970aca2a 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-list-utils" PKG_REQUIRE_NAME = "@kbn/securitysolution-list-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-list-utils" SOURCE_FILES = glob( [ @@ -82,11 +81,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -118,7 +115,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-list-utils/tsconfig.json b/packages/kbn-securitysolution-list-utils/tsconfig.json index fa50bd7981214f..305954d669b758 100644 --- a/packages/kbn-securitysolution-list-utils/tsconfig.json +++ b/packages/kbn-securitysolution-list-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-list-utils/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-rules/BUILD.bazel b/packages/kbn-securitysolution-rules/BUILD.bazel index 7158f759f4466d..80a27a426fbb26 100644 --- a/packages/kbn-securitysolution-rules/BUILD.bazel +++ b/packages/kbn-securitysolution-rules/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-rules" PKG_REQUIRE_NAME = "@kbn/securitysolution-rules" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-rules" SOURCE_FILES = glob( [ @@ -63,11 +62,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -98,7 +95,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-rules/tsconfig.json b/packages/kbn-securitysolution-rules/tsconfig.json index 3895e13ad28ede..305954d669b758 100644 --- a/packages/kbn-securitysolution-rules/tsconfig.json +++ b/packages/kbn-securitysolution-rules/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-rules/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel index e0898f90d7f870..6ee620199a6e6e 100644 --- a/packages/kbn-securitysolution-t-grid/BUILD.bazel +++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-t-grid" PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-t-grid" SOURCE_FILES = glob( [ @@ -72,11 +71,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -107,7 +104,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json index 3c701d149ab2e4..305954d669b758 100644 --- a/packages/kbn-securitysolution-t-grid/tsconfig.json +++ b/packages/kbn-securitysolution-t-grid/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index d22e31daacd55b..cfb6b722ea2e63 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-securitysolution-utils" PKG_REQUIRE_NAME = "@kbn/securitysolution-utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__securitysolution-utils" SOURCE_FILES = glob( [ @@ -61,11 +60,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -96,7 +93,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-utils/tsconfig.json b/packages/kbn-securitysolution-utils/tsconfig.json index 23fdf3178e174c..305954d669b758 100644 --- a/packages/kbn-securitysolution-utils/tsconfig.json +++ b/packages/kbn-securitysolution-utils/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-utils/src", "types": [ "jest", "node" diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel index e524078a8ba89e..29ca48adc566e2 100644 --- a/packages/kbn-server-http-tools/BUILD.bazel +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-server-http-tools" PKG_REQUIRE_NAME = "@kbn/server-http-tools" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__server-http-tools" SOURCE_FILES = glob( [ @@ -72,10 +71,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -107,7 +104,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index e378e41c3828bf..c89835eada8189 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target/types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-server-http-tools/src", "types": [ "jest", "node" diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index a0e1cf41dcf8f1..06c09260e2fa60 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-server-route-repository" PKG_REQUIRE_NAME = "@kbn/server-route-repository" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__server-route-repository" SOURCE_FILES = glob( [ @@ -68,10 +67,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -103,7 +100,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 447a2084926c69..db908eacc6c35c 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-server-route-repository/src", "stripInternal": false, "types": [ "jest", diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel index 1e45803dbdcf17..08cfa2a2a1308e 100644 --- a/packages/kbn-std/BUILD.bazel +++ b/packages/kbn-std/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-std" PKG_REQUIRE_NAME = "@kbn/std" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__std" SOURCE_FILES = glob( [ @@ -67,10 +66,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -102,7 +99,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index 2674ca26e96d52..04d1a54cc39517 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-std/src", "stripInternal": true, "types": [ "jest", diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index 686de744b656f7..ed7b167a30078e 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-storybook" PKG_REQUIRE_NAME = "@kbn/storybook" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__storybook" SOURCE_FILES = glob( [ @@ -93,11 +92,9 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -128,7 +125,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 0ccf3e78c82880..53e689b569e5e5 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -2,14 +2,11 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "incremental": false, "outDir": "target_types", "rootDir": "src", "skipLibCheck": true, - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-storybook", "target": "es2015", "types": ["node"] }, diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index 1f53e4b71ae212..50336dc44da005 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-telemetry-tools" PKG_REQUIRE_NAME = "@kbn/telemetry-tools" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__telemetry-tools" SOURCE_FILES = glob( [ @@ -71,10 +70,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -106,7 +103,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 034d7c0c6e745a..25e1d341ac07b3 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -2,13 +2,10 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "isolatedModules": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", "types": [ "jest", "node" diff --git a/packages/kbn-test-jest-helpers/BUILD.bazel b/packages/kbn-test-jest-helpers/BUILD.bazel index c713e245929449..d910fab5295d58 100644 --- a/packages/kbn-test-jest-helpers/BUILD.bazel +++ b/packages/kbn-test-jest-helpers/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-test-jest-helpers" PKG_REQUIRE_NAME = "@kbn/test-jest-helpers" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__test-jest-helpers" SOURCE_FILES = glob( [ @@ -133,10 +132,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -168,7 +165,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-test-jest-helpers/tsconfig.json b/packages/kbn-test-jest-helpers/tsconfig.json index 7a1121c9e91f13..72d996c69e2dac 100644 --- a/packages/kbn-test-jest-helpers/tsconfig.json +++ b/packages/kbn-test-jest-helpers/tsconfig.json @@ -2,13 +2,10 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "stripInternal": true, "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../../../packages/kbn-test-jest-helpers/src", "types": ["jest", "node"] }, "include": ["src/**/*"], diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 233aeab6250b19..6732b08d8bc72a 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-test" PKG_REQUIRE_NAME = "@kbn/test" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__test" SOURCE_FILES = glob( [ @@ -139,10 +138,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -174,7 +171,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 388d578c9af574..6e4fc2fb14628f 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -244,9 +244,10 @@ export function createTestEsCluster< esArgs: assignArgs(esArgs, overriddenArgs), esJavaOpts, // If we have multiple nodes, we shouldn't try setting up the native realm - // right away, or ES will complain as the cluster isn't ready. So we only + // right away or wait for ES to be green, the cluster isn't ready. So we only // set it up after the last node is started. skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1, + skipReadyCheck: this.nodes.length > 1 && i < this.nodes.length - 1, }); }); } diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index f70123029e6c48..717f214211d95d 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -228,8 +228,8 @@ export class KbnClientSavedObjects { await this.requester.request({ method: 'DELETE', path: options.space - ? uriencode`/s/${options.space}/api/saved_objects/${obj.type}/${obj.id}` - : uriencode`/api/saved_objects/${obj.type}/${obj.id}`, + ? uriencode`/s/${options.space}/api/saved_objects/${obj.type}/${obj.id}?force=true` + : uriencode`/api/saved_objects/${obj.type}/${obj.id}?force=true`, }); deleted++; } catch (error) { diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 7ba83019b00756..aa9fb4f04135d1 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -2,13 +2,10 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "stripInternal": true, "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../../../packages/kbn-test/src", "types": ["jest", "node"] }, "include": ["src/**/*", "index.d.ts"], diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index 62fd6adf5bb269..c2a5aa84dbb2d9 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-typed-react-router-config" PKG_REQUIRE_NAME = "@kbn/typed-react-router-config" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__typed-react-router-config" SOURCE_FILES = glob( [ @@ -83,10 +82,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -118,7 +115,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index 8e17781119ee9e..2619b0ff8f9d3c 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "isolatedModules": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", "stripInternal": true, "types": [ "node", diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel index 22d51e260bcc07..17bbb09bd36e32 100644 --- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-ui-shared-deps-npm" PKG_REQUIRE_NAME = "@kbn/ui-shared-deps-npm" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-shared-deps-npm" SOURCE_FILES = glob( [ @@ -121,11 +120,9 @@ ts_project( deps = TYPES_DEPS, allow_js = True, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -174,7 +171,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ui-shared-deps-npm/tsconfig.json b/packages/kbn-ui-shared-deps-npm/tsconfig.json index 107d82aa59ee8c..a8a821708d036f 100644 --- a/packages/kbn-ui-shared-deps-npm/tsconfig.json +++ b/packages/kbn-ui-shared-deps-npm/tsconfig.json @@ -3,12 +3,9 @@ "compilerOptions": { "allowJs": true, "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-ui-shared-deps-npm/src", "types": [ "node", ] diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index 3617956b15c4a6..295f6fa0594edb 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -5,7 +5,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-ui-shared-deps-src" PKG_REQUIRE_NAME = "@kbn/ui-shared-deps-src" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-shared-deps-src" SOURCE_FILES = glob( [ @@ -77,11 +76,9 @@ ts_project( deps = TYPES_DEPS, allow_js = True, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", - source_map = True, tsconfig = ":tsconfig", ) @@ -130,7 +127,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ui-shared-deps-src/tsconfig.json b/packages/kbn-ui-shared-deps-src/tsconfig.json index 521fb122e4659d..a8a821708d036f 100644 --- a/packages/kbn-ui-shared-deps-src/tsconfig.json +++ b/packages/kbn-ui-shared-deps-src/tsconfig.json @@ -3,12 +3,9 @@ "compilerOptions": { "allowJs": true, "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-ui-shared-deps-src/src", "types": [ "node", ] diff --git a/packages/kbn-ui-theme/BUILD.bazel b/packages/kbn-ui-theme/BUILD.bazel index 8e388efe23757d..5a848ddcc838f8 100644 --- a/packages/kbn-ui-theme/BUILD.bazel +++ b/packages/kbn-ui-theme/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-ui-theme" PKG_REQUIRE_NAME = "@kbn/ui-theme" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-theme" SOURCE_FILES = glob( [ @@ -61,10 +60,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -96,7 +93,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ui-theme/tsconfig.json b/packages/kbn-ui-theme/tsconfig.json index e1c27e88f1c91f..0fd9a15b5dbf88 100644 --- a/packages/kbn-ui-theme/tsconfig.json +++ b/packages/kbn-ui-theme/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-ui-theme/src", "stripInternal": true, "types": ["node"] }, diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index 159ab134684f88..c556751d7550e5 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-utility-types" PKG_REQUIRE_NAME = "@kbn/utility-types" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__utility-types" SOURCE_FILES = glob([ "src/jest/index.ts", @@ -56,10 +55,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -91,7 +88,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 997bcf9e0c45bd..1d7104a6fc2546 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -2,12 +2,9 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "./src", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-utility-types", "stripInternal": true, "types": [ "jest", diff --git a/packages/kbn-utils/BUILD.bazel b/packages/kbn-utils/BUILD.bazel index 6ac23129a1d03d..b60c60af43c25a 100644 --- a/packages/kbn-utils/BUILD.bazel +++ b/packages/kbn-utils/BUILD.bazel @@ -4,7 +4,6 @@ load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", PKG_BASE_NAME = "kbn-utils" PKG_REQUIRE_NAME = "@kbn/utils" -TYPES_PKG_REQUIRE_NAME = "@types/kbn__utils" SOURCE_FILES = glob( [ @@ -60,10 +59,8 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, - declaration_map = True, emit_declaration_only = True, out_dir = "target_types", - source_map = True, root_dir = "src", tsconfig = ":tsconfig", ) @@ -95,7 +92,7 @@ pkg_npm_types( name = "npm_module_types", srcs = SRCS, deps = [":tsc_types"], - package_name = TYPES_PKG_REQUIRE_NAME, + package_name = PKG_REQUIRE_NAME, tsconfig = ":tsconfig", visibility = ["//visibility:public"], ) diff --git a/packages/kbn-utils/tsconfig.json b/packages/kbn-utils/tsconfig.json index 85c26f42a695cd..a2851d55c0a456 100644 --- a/packages/kbn-utils/tsconfig.json +++ b/packages/kbn-utils/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, - "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-utils/src", "types": [ "jest", "node" diff --git a/renovate.json b/renovate.json index 95358af48a2cc6..9b673a5a9ccf65 100644 --- a/renovate.json +++ b/renovate.json @@ -35,14 +35,14 @@ "matchPackageNames": ["@elastic/elasticsearch"], "reviewers": ["team:kibana-operations", "team:kibana-core"], "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "Team:Operations", "Team:Core", "v8.1.0"], + "labels": ["release_note:skip", "backport:skip", "Team:Operations", "Team:Core"], "enabled": true }, { "groupName": "@elastic/elasticsearch", "matchPackageNames": ["@elastic/elasticsearch"], "reviewers": ["team:kibana-operations", "team:kibana-core"], - "matchBaseBranches": ["7.16"], + "matchBaseBranches": ["8.1"], "labels": ["release_note:skip", "Team:Operations", "Team:Core", "backport:skip"], "enabled": true }, @@ -50,7 +50,7 @@ "groupName": "@elastic/elasticsearch", "matchPackageNames": ["@elastic/elasticsearch"], "reviewers": ["team:kibana-operations", "team:kibana-core"], - "matchBaseBranches": ["7.15"], + "matchBaseBranches": ["7.17"], "labels": ["release_note:skip", "Team:Operations", "Team:Core", "backport:skip"], "enabled": true }, diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 6cfd1d57e9c96f..972c682ab0d9f8 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -9,6 +9,7 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), + require.resolve('../test/functional_ccs/config.js'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.ts'), diff --git a/src/core/public/core_app/errors/public_base_url.test.tsx b/src/core/public/core_app/errors/public_base_url.test.tsx index d1fb5a5093f150..805eb57aa51e0f 100644 --- a/src/core/public/core_app/errors/public_base_url.test.tsx +++ b/src/core/public/core_app/errors/public_base_url.test.tsx @@ -85,7 +85,7 @@ describe('publicBaseUrl warning', () => { }); expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ - title: 'Configuration missing', + title: 'Configuration recommended', text: expect.any(Function), }); }); diff --git a/src/core/public/core_app/errors/public_base_url.tsx b/src/core/public/core_app/errors/public_base_url.tsx index 3a4cce58bb1c41..59c9577470d266 100644 --- a/src/core/public/core_app/errors/public_base_url.tsx +++ b/src/core/public/core_app/errors/public_base_url.tsx @@ -44,23 +44,23 @@ export const setupPublicBaseUrlConfigWarning = ({ } const toast = notifications.toasts.addWarning({ - title: i18n.translate('core.ui.publicBaseUrlWarning.configMissingTitle', { - defaultMessage: 'Configuration missing', + title: i18n.translate('core.ui.publicBaseUrlWarning.configRecommendedTitle', { + defaultMessage: 'Configuration recommended', }), text: mountReactNode( <>

server.publicBaseUrl, }} />{' '}

diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 719535133e7ab5..c4e8e7e3642482 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -30,6 +30,13 @@ interface Deps { export class PluginsStatusService { private readonly pluginStatuses = new Map>(); + private readonly derivedStatuses = new Map>(); + private readonly dependenciesStatuses = new Map< + PluginName, + Observable> + >(); + private allPluginsStatuses?: Observable>; + private readonly update$ = new BehaviorSubject(true); private readonly defaultInheritedStatus$: Observable; private newRegistrationsAllowed = true; @@ -59,7 +66,10 @@ export class PluginsStatusService { } public getAll$(): Observable> { - return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); + if (!this.allPluginsStatuses) { + this.allPluginsStatuses = this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); + } + return this.allPluginsStatuses; } public getDependenciesStatus$(plugin: PluginName): Observable> { @@ -67,35 +77,46 @@ export class PluginsStatusService { if (!dependencies) { throw new Error(`Unknown plugin: ${plugin}`); } - - return this.getPluginStatuses$(dependencies).pipe( - // Prevent many emissions at once from dependency status resolution from making this too noisy - debounceTime(25) - ); + if (!this.dependenciesStatuses.has(plugin)) { + this.dependenciesStatuses.set( + plugin, + this.getPluginStatuses$(dependencies).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(25) + ) + ); + } + return this.dependenciesStatuses.get(plugin)!; } public getDerivedStatus$(plugin: PluginName): Observable { - return this.update$.pipe( - debounceTime(25), // Avoid calling the plugin's custom status logic for every plugin that depends on it. - switchMap(() => { - // Only go up the dependency tree if any of this plugin's dependencies have a custom status - // Helps eliminate memory overhead of creating thousands of Observables unnecessarily. - if (this.anyCustomStatuses(plugin)) { - return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( - map(([coreStatus, pluginStatuses]) => { - return getSummaryStatus( - [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], - { - allAvailableSummary: `All dependencies are available`, - } + if (!this.derivedStatuses.has(plugin)) { + this.derivedStatuses.set( + plugin, + this.update$.pipe( + debounceTime(25), // Avoid calling the plugin's custom status logic for every plugin that depends on it. + switchMap(() => { + // Only go up the dependency tree if any of this plugin's dependencies have a custom status + // Helps eliminate memory overhead of creating thousands of Observables unnecessarily. + if (this.anyCustomStatuses(plugin)) { + return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( + map(([coreStatus, pluginStatuses]) => { + return getSummaryStatus( + [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], + { + allAvailableSummary: `All dependencies are available`, + } + ); + }) ); - }) - ); - } else { - return this.defaultInheritedStatus$; - } - }) - ); + } else { + return this.defaultInheritedStatus$; + } + }) + ) + ); + } + return this.derivedStatuses.get(plugin)!; } private getPluginStatuses$(plugins: PluginName[]): Observable> { diff --git a/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl b/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl index 4651e9264ef501..ed48228bc95871 100644 --- a/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl +++ b/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl @@ -33,6 +33,9 @@ def _collect_inputs_deps_and_transitive_types_deps(ctx): deps_files = depset(transitive = deps_files_depsets).to_list() return [deps_files, transitive_types_deps] +def _get_type_package_name(actualName): + return "@types/" + actualName.replace("@", "").replace("/", "__") + def _calculate_entrypoint_path(ctx): return _join(ctx.bin_dir.path, ctx.label.package, _get_types_outdir_name(ctx), ctx.attr.entrypoint_name) @@ -78,7 +81,7 @@ def _pkg_npm_types_impl(ctx): # gathering template args template_args = [ - "NAME", ctx.attr.package_name + "NAME", _get_type_package_name(ctx.attr.package_name) ] # layout api extractor arguments @@ -119,7 +122,7 @@ def _pkg_npm_types_impl(ctx): deps = transitive_types_deps, ), LinkablePackageInfo( - package_name = ctx.attr.package_name, + package_name = _get_type_package_name(ctx.attr.package_name), package_path = "", path = package_dir.path, files = package_dir_depset, diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts index 7a15dce3af0195..8d49702b481544 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts @@ -8,17 +8,8 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import type { Subscription } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { PainlessLang } from '@kbn/monaco'; -import { - fieldValidators, - FieldConfig, - RuntimeType, - ValidationFunc, - ValidationCancelablePromise, -} from '../../shared_imports'; +import { fieldValidators, FieldConfig, RuntimeType, ValidationFunc } from '../../shared_imports'; import type { Context } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; @@ -32,42 +23,6 @@ const i18nTexts = { ), }; -// Validate the painless **syntax** (no need to make an HTTP request) -const painlessSyntaxValidator = () => { - let isValidatingSub: Subscription; - - return (() => { - const promise: ValidationCancelablePromise<'ERR_PAINLESS_SYNTAX'> = new Promise((resolve) => { - isValidatingSub = PainlessLang.validation$() - .pipe( - first(({ isValidating }) => { - return isValidating === false; - }) - ) - .subscribe(({ errors }) => { - const editorHasSyntaxErrors = errors.length > 0; - - if (editorHasSyntaxErrors) { - return resolve({ - message: i18nTexts.invalidScriptErrorMessage, - code: 'ERR_PAINLESS_SYNTAX', - }); - } - - resolve(undefined); - }); - }); - - promise.cancel = () => { - if (isValidatingSub) { - isValidatingSub.unsubscribe(); - } - }; - - return promise; - }) as ValidationFunc; -}; - // Validate the painless **script** const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => { const previewError = (await provider()) as Context['error']; @@ -131,10 +86,6 @@ export const schema = { ) ), }, - { - validator: painlessSyntaxValidator(), - isAsync: true, - }, { validator: painlessScriptValidator, isAsync: true, diff --git a/src/plugins/data_view_field_editor/public/open_editor.tsx b/src/plugins/data_view_field_editor/public/open_editor.tsx index c66e8183b9ab64..d3a91739dc94a1 100644 --- a/src/plugins/data_view_field_editor/public/open_editor.tsx +++ b/src/plugins/data_view_field_editor/public/open_editor.tsx @@ -19,6 +19,7 @@ import { UsageCollectionStart, DataViewsPublicPluginStart, FieldFormatsStart, + RuntimeType, } from './shared_imports'; import type { PluginStart, InternalFieldType, CloseEditor } from './types'; @@ -104,7 +105,12 @@ export const getFieldEditorOpener = } const isNewRuntimeField = !fieldName; - const isExistingRuntimeField = field && field.runtimeField && !field.isMapped; + const isExistingRuntimeField = + field && + field.runtimeField && + !field.isMapped && + // treat composite field instances as mapped fields for field editing purposes + field.runtimeField.type !== ('composite' as RuntimeType); const fieldTypeToProcess: InternalFieldType = isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete'; diff --git a/src/plugins/data_view_management/kibana.json b/src/plugins/data_view_management/kibana.json index 29f305d0ad17a5..a8a0f694bd1791 100644 --- a/src/plugins/data_view_management/kibana.json +++ b/src/plugins/data_view_management/kibana.json @@ -5,6 +5,7 @@ "ui": true, "requiredPlugins": ["management", "data", "urlForwarding", "dataViewFieldEditor", "dataViewEditor", "dataViews", "fieldFormats"], "requiredBundles": ["kibanaReact", "kibanaUtils"], + "optionalPlugins": ["spaces"], "owner": { "name": "App Services", "githubTeam": "kibana-app-services" diff --git a/src/plugins/data_view_management/public/components/__snapshots__/utils.test.ts.snap b/src/plugins/data_view_management/public/components/__snapshots__/utils.test.ts.snap index 3a25a78472b502..dece3790016439 100644 --- a/src/plugins/data_view_management/public/components/__snapshots__/utils.test.ts.snap +++ b/src/plugins/data_view_management/public/components/__snapshots__/utils.test.ts.snap @@ -5,6 +5,7 @@ Array [ Object { "default": true, "id": "test", + "namespaces": undefined, "sort": "0test name", "tags": Array [ Object { @@ -17,6 +18,7 @@ Array [ Object { "default": false, "id": "test1", + "namespaces": undefined, "sort": "1test name 1", "tags": Array [], "title": "test name 1", diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 5d5ef260a088cd..86193f50b2fe29 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -17,11 +17,12 @@ import { EuiText, EuiLink, EuiCallOut, + EuiCode, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataView, DataViewField } from '../../../../../plugins/data_views/public'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana, toMountPoint } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; @@ -50,7 +51,7 @@ const confirmModalOptionsDelete = { defaultMessage: 'Delete', }), title: i18n.translate('indexPatternManagement.editDataView.deleteHeader', { - defaultMessage: 'Delete data view?', + defaultMessage: 'Delete data view', }), }; @@ -110,11 +111,26 @@ export const EditIndexPattern = withRouter( } } - overlays.openConfirm('', confirmModalOptionsDelete).then((isConfirmed) => { - if (isConfirmed) { - doRemove(); - } - }); + const warning = + indexPattern.namespaces.length > 1 ? ( + {indexPattern.title}, + }} + /> + ) : ( + '' + ); + + overlays + .openConfirm(toMountPoint(
{warning}
), confirmModalOptionsDelete) + .then((isConfirmed) => { + if (isConfirmed) { + doRemove(); + } + }); }; const timeFilterHeader = i18n.translate( diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index abed44135e6eae..843c90d4f617b7 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -143,6 +143,9 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should }, Object { "displayName": "runtime", + "esTypes": Array [ + "long", + ], "excluded": false, "format": "", "hasRuntime": true, @@ -219,6 +222,9 @@ exports[`IndexedFieldsTable should filter based on the schema filter 1`] = ` Array [ Object { "displayName": "runtime", + "esTypes": Array [ + "long", + ], "excluded": false, "format": "", "hasRuntime": true, @@ -356,6 +362,9 @@ exports[`IndexedFieldsTable should render normally 1`] = ` }, Object { "displayName": "runtime", + "esTypes": Array [ + "long", + ], "excluded": false, "format": "", "hasRuntime": true, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index b2197a6dcb203b..44385c7b6e41d8 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DataView } from 'src/plugins/data_views/public'; import { IndexedFieldItem } from '../../types'; -import { Table, renderFieldName, getConflictModalContent } from './table'; +import { Table, renderFieldName, getConflictModalContent, showDelete } from './table'; import { overlayServiceMock, themeServiceMock } from 'src/core/public/mocks'; const theme = themeServiceMock.createStartContract(); @@ -198,4 +198,42 @@ describe('Table', () => { }) ).toMatchSnapshot(); }); + + test('showDelete', () => { + const runtimeFields = [ + { + name: 'customer', + info: [], + excluded: false, + kbnType: 'string', + type: 'keyword', + isMapped: false, + isUserEditable: true, + hasRuntime: true, + runtimeField: { + type: 'keyword', + }, + }, + { + name: 'thing', + info: [], + excluded: false, + kbnType: 'string', + type: 'keyword', + isMapped: false, + isUserEditable: true, + hasRuntime: true, + runtimeField: { + type: 'composite', + }, + }, + ] as IndexedFieldItem[]; + + // indexed field + expect(showDelete(items[0])).toBe(false); + // runtime field - primitive type + expect(showDelete(runtimeFields[0])).toBe(true); + // runtime field - composite type + expect(showDelete(runtimeFields[1])).toBe(false); + }); }); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index f860d9fafb1c0a..f8ce07b11d3e87 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -33,6 +33,9 @@ import { toMountPoint } from '../../../../../../../kibana_react/public'; import { DataView } from '../../../../../../../data_views/public'; import { IndexedFieldItem } from '../../types'; +export const showDelete = (field: IndexedFieldItem) => + !field.isMapped && field.isUserEditable && field.runtimeField?.type !== 'composite'; + // localized labels const additionalInfoAriaLabel = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.table.additionalInfoAriaLabel', @@ -454,7 +457,7 @@ export class Table extends PureComponent { onClick: (field) => deleteField(field.name), type: 'icon', 'data-test-subj': 'deleteField', - available: (field) => !field.isMapped && field.isUserEditable, + available: showDelete, }, ], width: '40px', diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index c7b92c227a5d9f..ff03bd404b6c42 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -94,6 +94,8 @@ const fields = [ name: 'runtime', displayName: 'runtime', runtimeField, + esTypes: ['long'], + type: 'number', }, ].map(mockFieldToIndexPatternField); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 965fcdb6d8818b..583a8255c02aba 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -18,13 +18,15 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { RouteComponentProps, withRouter, useLocation } from 'react-router-dom'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { IndexPatternTableItem } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; +import { SpacesList } from './spaces_list'; +import type { SpacesContextProps } from '../../../../../../x-pack/plugins/spaces/public'; const pagination = { initialPageSize: 10, @@ -38,15 +40,6 @@ const sorting = { }, }; -const search = { - box: { - incremental: true, - schema: { - fields: { title: { type: 'string' } }, - }, - }, -}; - const title = i18n.translate('indexPatternManagement.dataViewTable.title', { defaultMessage: 'Data Views', }); @@ -69,6 +62,8 @@ interface Props extends RouteComponentProps { showCreateDialog?: boolean; } +const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}; + export const IndexPatternTable = ({ history, canSave, @@ -81,20 +76,45 @@ export const IndexPatternTable = ({ chrome, dataViews, IndexPatternEditor, + spaces, } = useKibana().services; + const [query, setQuery] = useState(''); const [indexPatterns, setIndexPatterns] = useState([]); const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); const [showCreateDialog, setShowCreateDialog] = useState(showCreateDialogProp); + const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => { + if (!error) { + setQuery(queryText); + } + }; + + const search = { + query, + onChange: handleOnChange, + box: { + incremental: true, + schema: { + fields: { title: { type: 'string' } }, + }, + }, + }; + + const loadDataViews = useCallback(async () => { + setIsLoadingIndexPatterns(true); + const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( + uiSettings.get('defaultIndex'), + dataViews + ); + setIndexPatterns(gettedIndexPatterns); + setIsLoadingIndexPatterns(false); + return gettedIndexPatterns; + }, [dataViews, uiSettings]); + setBreadcrumbs(getListBreadcrumbs()); useEffect(() => { (async function () { - const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - uiSettings.get('defaultIndex'), - dataViews - ); - setIndexPatterns(gettedIndexPatterns); - setIsLoadingIndexPatterns(false); + const gettedIndexPatterns = await loadDataViews(); if ( gettedIndexPatterns.length === 0 || !(await dataViews.hasUserDataView().catch(() => false)) @@ -102,52 +122,64 @@ export const IndexPatternTable = ({ setShowCreateDialog(true); } })(); - }, [indexPatternManagementStart, uiSettings, dataViews]); + }, [indexPatternManagementStart, uiSettings, dataViews, loadDataViews]); chrome.docTitle.change(title); const isRollup = new URLSearchParams(useLocation().search).get('type') === 'rollup'; + const ContextWrapper = useMemo( + () => (spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent), + [spaces] + ); + const columns = [ { field: 'title', name: i18n.translate('indexPatternManagement.dataViewTable.nameColumn', { defaultMessage: 'Name', }), - render: ( - name: string, - index: { - id: string; - tags?: Array<{ - key: string; - name: string; - }>; - } - ) => ( + render: (name: string, dataView: IndexPatternTableItem) => ( <> - + {name} - {index.id && index.id.indexOf(securitySolution) === 0 && ( + {dataView?.id?.indexOf(securitySolution) === 0 && ( {securityDataView} )} - {index.tags && - index.tags.map(({ key: tagKey, name: tagName }) => ( - - {tagName} - - ))} + {dataView?.tags?.map(({ key: tagKey, name: tagName }) => ( + + {tagName} + + ))} ), dataType: 'string' as const, sortable: ({ sort }: { sort: string }) => sort, }, + { + field: 'namespaces', + name: 'spaces', + render: (name: string, dataView: IndexPatternTableItem) => { + return spaces ? ( + + ) : ( + <> + ); + }, + }, ]; const createButton = canSave ? ( @@ -197,17 +229,18 @@ export const IndexPatternTable = ({ /> - - + + + {displayIndexPatternEditor} ); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/spaces_list.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/spaces_list.tsx new file mode 100644 index 00000000000000..c17e174ef1ddab --- /dev/null +++ b/src/plugins/data_view_management/public/components/index_pattern_table/spaces_list.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC, useState } from 'react'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { + SpacesPluginStart, + ShareToSpaceFlyoutProps, +} from '../../../../../../x-pack/plugins/spaces/public'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data_views/public'; + +interface Props { + spacesApi: SpacesPluginStart; + spaceIds: string[]; + id: string; + title: string; + refresh(): void; +} + +const noun = i18n.translate('indexPatternManagement.indexPatternTable.savedObjectName', { + defaultMessage: 'data view', +}); + +export const SpacesList: FC = ({ spacesApi, spaceIds, id, title, refresh }) => { + const [showFlyout, setShowFlyout] = useState(false); + + function onClose() { + setShowFlyout(false); + refresh(); + } + + const LazySpaceList = spacesApi.ui.components.getSpaceList; + const LazyShareToSpaceFlyout = spacesApi.ui.components.getShareToSpaceFlyout; + + const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = { + savedObjectTarget: { + type: DATA_VIEW_SAVED_OBJECT_TYPE, + namespaces: spaceIds, + id, + title, + noun, + }, + onClose, + }; + + return ( + <> + setShowFlyout(true)} + style={{ height: 'auto' }} + data-test-subj="manageSpacesButton" + > + + + {showFlyout && } + + ); +}; diff --git a/src/plugins/data_view_management/public/components/types.ts b/src/plugins/data_view_management/public/components/types.ts index 3ead700732b91d..07ff0c7c160523 100644 --- a/src/plugins/data_view_management/public/components/types.ts +++ b/src/plugins/data_view_management/public/components/types.ts @@ -16,6 +16,7 @@ export interface IndexPatternTableItem { id: string; title: string; default: boolean; - tag?: string[]; + tags?: Array<{ key: string; name: string }>; sort: string; + namespaces?: string[]; } diff --git a/src/plugins/data_view_management/public/components/utils.ts b/src/plugins/data_view_management/public/components/utils.ts index 3024c172ac4410..1fccc8c694e318 100644 --- a/src/plugins/data_view_management/public/components/utils.ts +++ b/src/plugins/data_view_management/public/components/utils.ts @@ -32,18 +32,16 @@ const isRollup = (indexPatternType: string = '') => { return indexPatternType === 'rollup'; }; -export async function getIndexPatterns( - defaultIndex: string, - indexPatternsService: DataViewsContract -) { - const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); +export async function getIndexPatterns(defaultIndex: string, dataViewsService: DataViewsContract) { + const existingIndexPatterns = await dataViewsService.getIdsWithTitle(true); const indexPatternsListItems = existingIndexPatterns.map((idxPattern) => { - const { id, title } = idxPattern; + const { id, title, namespaces } = idxPattern; const isDefault = defaultIndex === id; const tags = getTags(idxPattern, isDefault); return { id, + namespaces, title, default: isDefault, tags, diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index e4978acbc9d172..1b5ae606bb19bf 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -40,7 +40,7 @@ export async function mountManagementSection( ) { const [ { chrome, uiSettings, notifications, overlays, http, docLinks, theme }, - { data, dataViewFieldEditor, dataViewEditor, dataViews, fieldFormats }, + { data, dataViewFieldEditor, dataViewEditor, dataViews, fieldFormats, spaces }, indexPatternManagementStart, ] = await getStartServices(); const canSave = dataViews.getCanSaveSync(); @@ -64,6 +64,7 @@ export async function mountManagementSection( fieldFormatEditors: dataViewFieldEditor.fieldFormatEditors, IndexPatternEditor: dataViewEditor.IndexPatternEditorComponent, fieldFormats, + spaces, }; ReactDOM.render( diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts index a0c25479ce3e25..84686dd666f9a1 100644 --- a/src/plugins/data_view_management/public/plugin.ts +++ b/src/plugins/data_view_management/public/plugin.ts @@ -16,6 +16,7 @@ import { ManagementSetup } from '../../management/public'; import { IndexPatternFieldEditorStart } from '../../data_view_field_editor/public'; import { DataViewEditorStart } from '../../data_view_editor/public'; import { DataViewsPublicPluginStart } from '../../data_views/public'; +import { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -28,6 +29,7 @@ export interface IndexPatternManagementStartDependencies { dataViewEditor: DataViewEditorStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; + spaces?: SpacesPluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index f0a79416892ef2..257d07cd478db3 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -22,6 +22,7 @@ import { IndexPatternFieldEditorStart } from '../../data_view_field_editor/publi import { DataViewEditorStart } from '../../data_view_editor/public'; import { DataViewsPublicPluginStart } from '../../data_views/public'; import { FieldFormatsStart } from '../../field_formats/public'; +import { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -38,6 +39,7 @@ export interface IndexPatternManagmentContext { fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; IndexPatternEditor: DataViewEditorStart['IndexPatternEditorComponent']; fieldFormats: FieldFormatsStart; + spaces?: SpacesPluginStart; } export type IndexPatternManagmentContextValue = diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index bde927aaf732be..9710111bcfde26 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -20,5 +20,6 @@ { "path": "../es_ui_shared/tsconfig.json" }, { "path": "../data_view_field_editor/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/src/plugins/data_views/common/constants.ts b/src/plugins/data_views/common/constants.ts index d656a044e10806..b5a73eee8b3d38 100644 --- a/src/plugins/data_views/common/constants.ts +++ b/src/plugins/data_views/common/constants.ts @@ -14,6 +14,7 @@ export const RUNTIME_FIELD_TYPES = [ 'ip', 'boolean', 'geo_point', + 'composite', ] as const; /** diff --git a/src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap b/src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap index 731b5ba6e260cb..0a9109c282922a 100644 --- a/src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap +++ b/src/plugins/data_views/common/data_views/__snapshots__/data_view.test.ts.snap @@ -1,5 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`IndexPattern addRuntimeField and removeRuntimeField add and remove composite runtime field as new fields 1`] = ` +Object { + "fields": Object { + "a": Object { + "type": "keyword", + }, + "b": Object { + "type": "long", + }, + }, + "script": Object { + "source": "emit('hello world');", + }, + "type": "composite", +} +`; + +exports[`IndexPattern addRuntimeField and removeRuntimeField add and remove runtime field as new field 1`] = ` +Object { + "script": Object { + "source": "emit('hello world');", + }, + "type": "keyword", +} +`; + exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { "allowNoIndex": false, diff --git a/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap b/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap index 50bc27b15922ac..09784f9de6a37c 100644 --- a/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap +++ b/src/plugins/data_views/common/data_views/__snapshots__/data_views.test.ts.snap @@ -40,6 +40,7 @@ Object { }, "fields": Object {}, "id": "id", + "namespaces": undefined, "runtimeFieldMap": Object { "aRuntimeField": Object { "script": Object { diff --git a/src/plugins/data_views/common/data_views/data_view.test.ts b/src/plugins/data_views/common/data_views/data_view.test.ts index 40a3a06c869ab3..514aba80bdb1e7 100644 --- a/src/plugins/data_views/common/data_views/data_view.test.ts +++ b/src/plugins/data_views/common/data_views/data_view.test.ts @@ -16,7 +16,7 @@ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../../field_formats/common/mocks'; import { FieldFormat } from '../../../field_formats/common'; -import { RuntimeField } from '../types'; +import { RuntimeField, RuntimeTypeExceptComposite } from '../types'; import { stubLogstashFields } from '../field.stub'; import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; @@ -37,6 +37,8 @@ const runtimeField = { name: 'runtime_field', runtimeField: runtimeFieldScript, scripted: false, + esTypes: ['keyword'], + type: 'string', }; fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; @@ -223,20 +225,80 @@ describe('IndexPattern', () => { }, }; + const runtimeWithAttrs = { + ...runtime, + popularity: 5, + customLabel: 'custom name', + format: { + id: 'bytes', + }, + }; + + const runtimeComposite = { + type: 'composite' as RuntimeField['type'], + script: { + source: "emit('hello world');", + }, + fields: { + a: { + type: 'keyword' as RuntimeTypeExceptComposite, + }, + b: { + type: 'long' as RuntimeTypeExceptComposite, + }, + }, + }; + + const runtimeCompositeWithAttrs = { + type: runtimeComposite.type, + script: runtimeComposite.script, + fields: { + a: { + ...runtimeComposite.fields.a, + popularity: 3, + customLabel: 'custom name a', + format: { + id: 'bytes', + }, + }, + b: { + ...runtimeComposite.fields.b, + popularity: 4, + customLabel: 'custom name b', + format: { + id: 'bytes', + }, + }, + }, + }; + beforeEach(() => { const formatter = { toJSON: () => ({ id: 'bytes' }), } as FieldFormat; indexPattern.getFormatterForField = () => formatter; + indexPattern.getFormatterForFieldNoDefault = () => undefined; }); test('add and remove runtime field to existing field', () => { - indexPattern.addRuntimeField('@tags', runtime); + indexPattern.addRuntimeField('@tags', runtimeWithAttrs); expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ '@tags': runtime, runtime_field: runtimeField.runtimeField, }); - expect(indexPattern.toSpec()!.fields!['@tags'].runtimeField).toEqual(runtime); + const field = indexPattern.toSpec()!.fields!['@tags']; + expect(field.runtimeField).toEqual(runtime); + expect(field.count).toEqual(5); + expect(field.format).toEqual({ + id: 'bytes', + }); + expect(field.customLabel).toEqual('custom name'); + expect(indexPattern.toSpec().fieldAttrs).toEqual({ + '@tags': { + customLabel: 'custom name', + count: 5, + }, + }); indexPattern.removeRuntimeField('@tags'); expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ @@ -246,11 +308,12 @@ describe('IndexPattern', () => { }); test('add and remove runtime field as new field', () => { - indexPattern.addRuntimeField('new_field', runtime); + indexPattern.addRuntimeField('new_field', runtimeWithAttrs); expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ runtime_field: runtimeField.runtimeField, new_field: runtime, }); + expect(indexPattern.getRuntimeField('new_field')).toMatchSnapshot(); expect(indexPattern.toSpec()!.fields!.new_field.runtimeField).toEqual(runtime); indexPattern.removeRuntimeField('new_field'); @@ -260,6 +323,35 @@ describe('IndexPattern', () => { expect(indexPattern.toSpec()!.fields!.new_field).toBeUndefined(); }); + test('add and remove composite runtime field as new fields', () => { + const fieldCount = indexPattern.fields.length; + indexPattern.addRuntimeField('new_field', runtimeCompositeWithAttrs); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + new_field: runtimeComposite, + }); + expect(indexPattern.fields.length - fieldCount).toEqual(2); + expect(indexPattern.getRuntimeField('new_field')).toMatchSnapshot(); + expect(indexPattern.toSpec()!.fields!['new_field.a']).toBeDefined(); + expect(indexPattern.toSpec()!.fields!['new_field.b']).toBeDefined(); + expect(indexPattern.toSpec()!.fieldAttrs).toEqual({ + 'new_field.a': { + count: 3, + customLabel: 'custom name a', + }, + 'new_field.b': { + count: 4, + customLabel: 'custom name b', + }, + }); + + indexPattern.removeRuntimeField('new_field'); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!.new_field).toBeUndefined(); + }); + test('should not allow runtime field with * in name', async () => { try { await indexPattern.addRuntimeField('test*123', runtime); diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index 67a127407f94a0..4349fc11bced5c 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -12,7 +12,7 @@ import _, { each, reject } from 'lodash'; import { castEsToKbnFieldTypeName, ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '..'; -import type { RuntimeField } from '../types'; +import type { RuntimeField, RuntimeFieldSpec, RuntimeType, FieldConfiguration } from '../types'; import { CharacterNotAllowedInField } from '../../../kibana_utils/common'; import { IIndexPattern, IFieldType } from '../../common'; @@ -24,6 +24,7 @@ import { SerializedFieldFormat, } from '../../../field_formats/common'; import { DataViewSpec, TypeMeta, SourceFilter, DataViewFieldMap } from '../types'; +import { removeFieldAttrs } from './utils'; interface DataViewDeps { spec?: DataViewSpec; @@ -75,11 +76,12 @@ export class DataView implements IIndexPattern { */ public version: string | undefined; public sourceFilters?: SourceFilter[]; + public namespaces: string[]; private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; private fieldAttrs: FieldAttrs; - private runtimeFieldMap: Record; + private runtimeFieldMap: Record; /** * prevents errors when index pattern exists before indices @@ -112,6 +114,7 @@ export class DataView implements IIndexPattern { this.fieldAttrs = spec.fieldAttrs || {}; this.allowNoIndex = spec.allowNoIndex || false; this.runtimeFieldMap = spec.runtimeFieldMap || {}; + this.namespaces = spec.namespaces || []; } /** @@ -184,11 +187,13 @@ export class DataView implements IIndexPattern { }; }); + const runtimeFields = this.getRuntimeMappings(); + return { storedFields: ['*'], scriptFields, docvalueFields, - runtimeFields: this.runtimeFieldMap, + runtimeFields, }; } @@ -316,31 +321,34 @@ export class DataView implements IIndexPattern { /** * Add a runtime field - Appended to existing mapped field or a new field is - * created as appropriate + * created as appropriate. * @param name Field name * @param runtimeField Runtime field definition */ - addRuntimeField(name: string, runtimeField: RuntimeField) { - const existingField = this.getFieldByName(name); - + addRuntimeField(name: string, runtimeField: RuntimeField): DataViewField[] { if (name.includes('*')) { throw new CharacterNotAllowedInField('*', name); } - if (existingField) { - existingField.runtimeField = runtimeField; - } else { - this.fields.add({ - name, - runtimeField, - type: castEsToKbnFieldTypeName(runtimeField.type), - aggregatable: true, - searchable: true, - count: 0, - readFromDocValues: false, - }); + const { type, script, customLabel, format, popularity } = runtimeField; + + if (type === 'composite') { + return this.addCompositeRuntimeField(name, runtimeField); } - this.runtimeFieldMap[name] = runtimeField; + + this.runtimeFieldMap[name] = removeFieldAttrs(runtimeField); + const field = this.updateOrAddRuntimeField( + name, + type, + { type, script }, + { + customLabel, + format, + popularity, + } + ); + + return [field]; } /** @@ -356,7 +364,60 @@ export class DataView implements IIndexPattern { * @param name */ getRuntimeField(name: string): RuntimeField | null { - return this.runtimeFieldMap[name] ?? null; + if (!this.runtimeFieldMap[name]) { + return null; + } + + const { type, script, fields } = { ...this.runtimeFieldMap[name] }; + const runtimeField: RuntimeField = { + type, + script, + }; + + if (type === 'composite') { + runtimeField.fields = fields; + } + + return runtimeField; + } + + getAllRuntimeFields(): Record { + return Object.keys(this.runtimeFieldMap).reduce>( + (acc, fieldName) => ({ + ...acc, + [fieldName]: this.getRuntimeField(fieldName)!, + }), + {} + ); + } + + getFieldsByRuntimeFieldName(name: string): Record | undefined { + const runtimeField = this.getRuntimeField(name); + if (!runtimeField) { + return; + } + + if (runtimeField.type === 'composite') { + return Object.entries(runtimeField.fields!).reduce>( + (acc, [subFieldName, subField]) => { + const fieldFullName = `${name}.${subFieldName}`; + const dataViewField = this.getFieldByName(fieldFullName); + + if (!dataViewField) { + // We should never enter here as all composite runtime subfield + // are converted to data view fields. + return acc; + } + acc[subFieldName] = dataViewField; + return acc; + }, + {} + ); + } + + const primitveRuntimeField = this.getFieldByName(name); + + return primitveRuntimeField && { [name]: primitveRuntimeField }; } /** @@ -381,17 +442,26 @@ export class DataView implements IIndexPattern { */ removeRuntimeField(name: string) { const existingField = this.getFieldByName(name); - if (existingField) { - if (existingField.isMapped) { - // mapped field, remove runtimeField def - existingField.runtimeField = undefined; - } else { - this.fields.remove(existingField); - } + + if (existingField && existingField.isMapped) { + // mapped field, remove runtimeField def + existingField.runtimeField = undefined; + } else { + Object.values(this.getFieldsByRuntimeFieldName(name) || {}).forEach((field) => { + this.fields.remove(field); + }); } delete this.runtimeFieldMap[name]; } + /** + * Return the "runtime_mappings" section of the ES search query + */ + getRuntimeMappings(): estypes.MappingRuntimeFields { + // @ts-expect-error The ES client does not yet include the "composite" runtime type + return _.cloneDeep(this.runtimeFieldMap); + } + /** * Get formatter for a given field name. Return undefined if none exists * @param field @@ -432,9 +502,7 @@ export class DataView implements IIndexPattern { if (fieldObject) { if (!newCount) fieldObject.deleteCount(); else fieldObject.count = newCount; - return; } - this.setFieldAttrs(fieldName, 'count', newCount); } @@ -445,6 +513,92 @@ export class DataView implements IIndexPattern { public readonly deleteFieldFormat = (fieldName: string) => { delete this.fieldFormatMap[fieldName]; }; + + private addCompositeRuntimeField(name: string, runtimeField: RuntimeField): DataViewField[] { + const { fields } = runtimeField; + + // Make sure subFields are provided + if (fields === undefined || Object.keys(fields).length === 0) { + throw new Error(`Can't add composite runtime field [name = ${name}] without subfields.`); + } + + // Make sure no field with the same name already exist + if (this.getFieldByName(name) !== undefined) { + throw new Error( + `Can't create composite runtime field ["${name}"] as there is already a field with this name` + ); + } + + // We first remove the runtime composite field with the same name which will remove all of its subFields. + // This guarantees that we don't leave behind orphan data view fields + this.removeRuntimeField(name); + + const runtimeFieldSpec = removeFieldAttrs(runtimeField); + + // We don't add composite runtime fields to the field list as + // they are not fields but **holder** of fields. + // What we do add to the field list are all their subFields. + const dataViewFields = Object.entries(fields).map(([subFieldName, subField]) => + // Every child field gets the complete runtime field script for consumption by searchSource + this.updateOrAddRuntimeField(`${name}.${subFieldName}`, subField.type, runtimeFieldSpec, { + customLabel: subField.customLabel, + format: subField.format, + popularity: subField.popularity, + }) + ); + + this.runtimeFieldMap[name] = removeFieldAttrs(runtimeField); + return dataViewFields; + } + + private updateOrAddRuntimeField( + fieldName: string, + fieldType: RuntimeType, + runtimeFieldSpec: RuntimeFieldSpec, + config: FieldConfiguration + ): DataViewField { + if (fieldType === 'composite') { + throw new Error( + `Trying to add composite field as primmitive field, this shouldn't happen! [name = ${fieldName}]` + ); + } + + // Create the field if it does not exist or update an existing one + let createdField: DataViewField | undefined; + const existingField = this.getFieldByName(fieldName); + + if (existingField) { + existingField.runtimeField = runtimeFieldSpec; + } else { + createdField = this.fields.add({ + name: fieldName, + runtimeField: runtimeFieldSpec, + type: castEsToKbnFieldTypeName(fieldType), + esTypes: [fieldType], + aggregatable: true, + searchable: true, + count: config.popularity ?? 0, + readFromDocValues: false, + }); + } + + // Apply configuration to the field + if (config.customLabel || config.customLabel === null) { + this.setFieldCustomLabel(fieldName, config.customLabel); + } + + if (config.popularity || config.popularity === null) { + this.setFieldCount(fieldName, config.popularity); + } + + if (config.format) { + this.setFieldFormat(fieldName, config.format); + } else if (config.format === null) { + this.deleteFieldFormat(fieldName); + } + + return createdField ?? existingField!; + } } /** diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 8ce6b00d131bb5..2e31ed793c3dbb 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -14,7 +14,7 @@ import { castEsToKbnFieldTypeName } from '@kbn/field-types'; import { DATA_VIEW_SAVED_OBJECT_TYPE, FLEET_ASSETS_TO_IGNORE, SavedObjectsClientCommon } from '..'; import { createDataViewCache } from '.'; -import type { RuntimeField } from '../types'; +import type { RuntimeField, RuntimeFieldSpec, RuntimeType } from '../types'; import { DataView } from './data_view'; import { createEnsureDefaultDataView, EnsureDefaultDataView } from './ensure_default_data_view'; import { @@ -48,6 +48,7 @@ export type IndexPatternListSavedObjectAttrs = Pick< export interface DataViewListItem { id: string; + namespaces?: string[]; title: string; type?: string; typeMeta?: TypeMeta; @@ -176,6 +177,7 @@ export class DataViewsService { } return this.savedObjectsCache.map((obj) => ({ id: obj?.id, + namespaces: obj?.namespaces, title: obj?.attributes?.title, type: obj?.attributes?.type, typeMeta: obj?.attributes?.typeMeta && JSON.parse(obj?.attributes?.typeMeta), @@ -374,6 +376,7 @@ export class DataViewsService { const { id, version, + namespaces, attributes: { title, timeFieldName, @@ -400,6 +403,7 @@ export class DataViewsService { return { id, version, + namespaces, title, timeFieldName, sourceFilters: parsedSourceFilters, @@ -450,20 +454,36 @@ export class DataViewsService { spec.fieldAttrs ); + const addRuntimeFieldToSpecFields = ( + name: string, + fieldType: RuntimeType, + runtimeField: RuntimeFieldSpec + ) => { + spec.fields![name] = { + name, + type: castEsToKbnFieldTypeName(fieldType), + esTypes: [fieldType], + runtimeField, + aggregatable: true, + searchable: true, + readFromDocValues: false, + customLabel: spec.fieldAttrs?.[name]?.customLabel, + count: spec.fieldAttrs?.[name]?.count, + }; + }; + // CREATE RUNTIME FIELDS - for (const [key, value] of Object.entries(runtimeFieldMap || {})) { + for (const [name, runtimeField] of Object.entries(runtimeFieldMap || {})) { // do not create runtime field if mapped field exists - if (!spec.fields[key]) { - spec.fields[key] = { - name: key, - type: castEsToKbnFieldTypeName(value.type), - runtimeField: value, - aggregatable: true, - searchable: true, - readFromDocValues: false, - customLabel: spec.fieldAttrs?.[key]?.customLabel, - count: spec.fieldAttrs?.[key]?.count, - }; + if (!spec.fields[name]) { + // For composite runtime field we add the subFields, **not** the composite + if (runtimeField.type === 'composite') { + Object.entries(runtimeField.fields!).forEach(([subFieldName, subField]) => { + addRuntimeFieldToSpecFields(`${name}.${subFieldName}`, subField.type, runtimeField); + }); + } else { + addRuntimeFieldToSpecFields(name, runtimeField.type, runtimeField); + } } } } catch (err) { diff --git a/src/plugins/data_views/common/data_views/utils.ts b/src/plugins/data_views/common/data_views/utils.ts new file mode 100644 index 00000000000000..31a6b573ff2b3c --- /dev/null +++ b/src/plugins/data_views/common/data_views/utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RuntimeField, RuntimeFieldSpec, RuntimeTypeExceptComposite } from '../types'; + +export const removeFieldAttrs = (runtimeField: RuntimeField): RuntimeFieldSpec => { + const { type, script, fields } = runtimeField; + const fieldsTypeOnly = fields && { + fields: Object.entries(fields).reduce((col, [fieldName, field]) => { + col[fieldName] = { type: field.type }; + return col; + }, {} as Record), + }; + + return { + type, + script, + ...fieldsTypeOnly, + }; +}; diff --git a/src/plugins/data_views/common/fields/data_view_field.test.ts b/src/plugins/data_views/common/fields/data_view_field.test.ts index 9c611354683c26..72fbd96d40b653 100644 --- a/src/plugins/data_views/common/fields/data_view_field.test.ts +++ b/src/plugins/data_views/common/fields/data_view_field.test.ts @@ -27,7 +27,7 @@ describe('Field', function () { script: 'script', lang: 'java' as const, count: 1, - esTypes: ['text'], // note, this will get replaced by the runtime field type + esTypes: ['keyword'], aggregatable: true, filterable: true, searchable: true, diff --git a/src/plugins/data_views/common/fields/data_view_field.ts b/src/plugins/data_views/common/fields/data_view_field.ts index ca74f0c52d2539..a10c4268888db8 100644 --- a/src/plugins/data_views/common/fields/data_view_field.ts +++ b/src/plugins/data_views/common/fields/data_view_field.ts @@ -8,9 +8,9 @@ /* eslint-disable max-classes-per-file */ -import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { KbnFieldType, getKbnFieldType } from '@kbn/field-types'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import type { RuntimeField } from '../types'; +import type { RuntimeFieldSpec } from '../types'; import type { IFieldType } from './types'; import { FieldSpec, DataView } from '..'; import { @@ -49,7 +49,7 @@ export class DataViewField implements IFieldType { return this.spec.runtimeField; } - public set runtimeField(runtimeField: RuntimeField | undefined) { + public set runtimeField(runtimeField: RuntimeFieldSpec | undefined) { this.spec.runtimeField = runtimeField; } @@ -108,13 +108,11 @@ export class DataViewField implements IFieldType { } public get type() { - return this.runtimeField?.type - ? castEsToKbnFieldTypeName(this.runtimeField?.type) - : this.spec.type; + return this.spec.type; } public get esTypes() { - return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes; + return this.spec.esTypes; } public get scripted() { @@ -144,6 +142,10 @@ export class DataViewField implements IFieldType { return this.spec.isMapped; } + public get isRuntimeField() { + return !this.isMapped && this.runtimeField !== undefined; + } + // not writable, not serialized public get sortable() { return ( @@ -228,6 +230,10 @@ export class DataViewField implements IFieldType { isMapped: this.isMapped, }; } + + public isRuntimeCompositeSubField() { + return this.runtimeField?.type === 'composite'; + } } /** diff --git a/src/plugins/data_views/common/fields/field_list.ts b/src/plugins/data_views/common/fields/field_list.ts index e2c850c0c4dd0d..c7ef23735d7bdf 100644 --- a/src/plugins/data_views/common/fields/field_list.ts +++ b/src/plugins/data_views/common/fields/field_list.ts @@ -15,7 +15,7 @@ import { DataView } from '../data_views'; type FieldMap = Map; export interface IIndexPatternFieldList extends Array { - add(field: FieldSpec): void; + add(field: FieldSpec): DataViewField; getAll(): DataViewField[]; getByName(name: DataViewField['name']): DataViewField | undefined; getByType(type: DataViewField['type']): DataViewField[]; @@ -55,11 +55,12 @@ export const fieldList = ( public readonly getByType = (type: DataViewField['type']) => [ ...(this.groups.get(type) || new Map()).values(), ]; - public readonly add = (field: FieldSpec) => { + public readonly add = (field: FieldSpec): DataViewField => { const newField = new DataViewField({ ...field, shortDotsEnable }); this.push(newField); this.setByName(newField); this.setByGroup(newField); + return newField; }; public readonly remove = (field: IFieldType) => { diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts index ece482ff79eaab..ab60fbf34071a2 100644 --- a/src/plugins/data_views/common/index.ts +++ b/src/plugins/data_views/common/index.ts @@ -28,6 +28,8 @@ export type { FieldFormatMap, RuntimeType, RuntimeField, + RuntimeFieldSpec, + RuntimeFieldSubField, IIndexPattern, DataViewAttributes, IndexPatternAttributes, diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index f991b61bfad5fb..8a1f7265296ff9 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -22,13 +22,49 @@ export type { QueryDslQueryContainer }; export type FieldFormatMap = Record; export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; -export interface RuntimeField { + +export type RuntimeTypeExceptComposite = Exclude; + +export interface RuntimeFieldBase { type: RuntimeType; script?: { source: string; }; } +/** + * The RuntimeField that will be sent in the ES Query "runtime_mappings" object + */ +export interface RuntimeFieldSpec extends RuntimeFieldBase { + fields?: Record< + string, + { + // It is not recursive, we can't create a composite inside a composite. + type: RuntimeTypeExceptComposite; + } + >; +} + +export interface FieldConfiguration { + format?: SerializedFieldFormat | null; + customLabel?: string; + popularity?: number; +} + +/** + * This is the RuntimeField interface enhanced with Data view field + * configuration: field format definition, customLabel or popularity. + * + * @see {@link RuntimeField} + */ +export interface RuntimeField extends RuntimeFieldBase, FieldConfiguration { + fields?: Record; +} + +export interface RuntimeFieldSubField extends FieldConfiguration { + type: RuntimeTypeExceptComposite; +} + /** * @deprecated * IIndexPattern allows for an IndexPattern OR an index pattern saved object @@ -216,7 +252,7 @@ export interface FieldSpec extends DataViewFieldBase { readFromDocValues?: boolean; indexed?: boolean; customLabel?: string; - runtimeField?: RuntimeField; + runtimeField?: RuntimeFieldSpec; // not persisted shortDotsEnable?: boolean; isMapped?: boolean; @@ -244,9 +280,10 @@ export interface DataViewSpec { typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; - runtimeFieldMap?: Record; + runtimeFieldMap?: Record; fieldAttrs?: FieldAttrs; allowNoIndex?: boolean; + namespaces?: string[]; } export interface SourceFilter { diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index 11b3edaa096288..2a9f1201cc854d 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -15,9 +15,15 @@ export { } from '../common/lib'; export { onRedirectNoIndexPattern } from './data_views'; -export type { IIndexPatternFieldList, TypeMeta } from '../common'; +export type { IIndexPatternFieldList, TypeMeta, RuntimeType } from '../common'; export type { DataViewSpec } from '../common'; -export { IndexPatternField, DataViewField, DataViewType, META_FIELDS } from '../common'; +export { + IndexPatternField, + DataViewField, + DataViewType, + META_FIELDS, + DATA_VIEW_SAVED_OBJECT_TYPE, +} from '../common'; export type { IndexPatternsContract } from './data_views'; export type { DataViewListItem } from './data_views'; diff --git a/src/plugins/data_views/public/saved_objects_client_wrapper.ts b/src/plugins/data_views/public/saved_objects_client_wrapper.ts index beaae6ac3fc217..a5bc83546e3bb9 100644 --- a/src/plugins/data_views/public/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/public/saved_objects_client_wrapper.ts @@ -54,6 +54,6 @@ export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommo return simpleSavedObjectToSavedObject(response); } delete(type: string, id: string) { - return this.savedObjectClient.delete(type, id); + return this.savedObjectClient.delete(type, id, { force: true }); } } diff --git a/src/plugins/data_views/server/rest_api_routes/create_data_view.ts b/src/plugins/data_views/server/rest_api_routes/create_data_view.ts index 4b103ba87662c8..00e6e94887f501 100644 --- a/src/plugins/data_views/server/rest_api_routes/create_data_view.ts +++ b/src/plugins/data_views/server/rest_api_routes/create_data_view.ts @@ -10,11 +10,7 @@ import { UsageCounter } from 'src/plugins/usage_collection/server'; import { schema } from '@kbn/config-schema'; import { DataViewSpec, DataViewsService } from 'src/plugins/data_views/common'; import { handleErrors } from './util/handle_errors'; -import { - fieldSpecSchema, - runtimeFieldSpecSchema, - serializedFieldFormatSchema, -} from './util/schemas'; +import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../core/server'; import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; import { @@ -71,7 +67,7 @@ const dataViewSpecSchema = schema.object({ ) ), allowNoIndex: schema.maybe(schema.boolean()), - runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSchema)), }); const registerCreateDataViewRouteFactory = diff --git a/src/plugins/data_views/server/rest_api_routes/fields/update_fields.test.ts b/src/plugins/data_views/server/rest_api_routes/fields/update_fields.test.ts index caf673ebbf3d97..453d6c90e5cc65 100644 --- a/src/plugins/data_views/server/rest_api_routes/fields/update_fields.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/fields/update_fields.test.ts @@ -23,6 +23,7 @@ describe('create runtime field', () => { fields: { getByName: jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce({}), }, + getFieldsByRuntimeFieldName: jest.fn().mockReturnValueOnce({}), } as unknown as DataView) ); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.test.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.test.ts index 555cbaad322528..ac2e6bc54fe15e 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.test.ts @@ -20,8 +20,17 @@ describe('create runtime field', () => { ({ addRuntimeField: jest.fn(), fields: { - getByName: jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce({}), + getByName: jest + .fn() + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce({}), }, + getRuntimeField: jest + .fn() + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce({}), } as unknown as DataView) ); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts index d8f20c9ea6dafa..8753e58c8e240a 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts @@ -10,7 +10,7 @@ import { UsageCounter } from 'src/plugins/usage_collection/server'; import { DataViewsService, RuntimeField } from 'src/plugins/data_views/common'; import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; -import { runtimeFieldSpecSchema } from '../util/schemas'; +import { runtimeFieldSchema } from '../util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataViewsServerPluginStart, @@ -45,18 +45,21 @@ export const createRuntimeField = async ({ usageCollection?.incrementCounter({ counterName }); const dataView = await dataViewsService.get(id); - if (dataView.fields.getByName(name)) { + if (dataView.fields.getByName(name) || dataView.getRuntimeField(name)) { throw new Error(`Field [name = ${name}] already exists.`); } - dataView.addRuntimeField(name, runtimeField); + const firstNameSegment = name.split('.')[0]; - const field = dataView.fields.getByName(name); - if (!field) throw new Error(`Could not create a field [name = ${name}].`); + if (dataView.fields.getByName(firstNameSegment) || dataView.getRuntimeField(firstNameSegment)) { + throw new Error(`Field [name = ${firstNameSegment}] already exists.`); + } + + const createdRuntimeFields = dataView.addRuntimeField(name, runtimeField); await dataViewsService.updateSavedObject(dataView); - return { dataView, field }; + return { dataView, fields: createdRuntimeFields }; }; const runtimeCreateFieldRouteFactory = @@ -84,7 +87,7 @@ const runtimeCreateFieldRouteFactory = minLength: 1, maxLength: 1_000, }), - runtimeField: runtimeFieldSpecSchema, + runtimeField: runtimeFieldSchema, }), }, }, @@ -100,16 +103,16 @@ const runtimeCreateFieldRouteFactory = const id = req.params.id; const { name, runtimeField } = req.body; - const { dataView, field } = await createRuntimeField({ + const { dataView, fields } = await createRuntimeField({ dataViewsService, usageCollection, counterName: `${req.route.method} ${path}`, id, name, - runtimeField, + runtimeField: runtimeField as RuntimeField, }); - return res.ok(responseFormatter({ serviceKey, dataView, field })); + return res.ok(responseFormatter({ serviceKey, dataView, fields })); }) ); }; diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.test.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.test.ts index 10fa4d63f34419..8262916e9ef4e5 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.test.ts @@ -19,11 +19,7 @@ describe('delete runtime field', () => { async (id: string) => ({ removeRuntimeField: jest.fn(), - fields: { - getByName: jest.fn().mockReturnValueOnce({ - runtimeField: {}, - }), - }, + getRuntimeField: jest.fn().mockReturnValueOnce({}), } as unknown as DataView) ); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.ts index 706fe49582b5a6..b846eb74771d9d 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/delete_runtime_field.ts @@ -35,16 +35,12 @@ export const deleteRuntimeField = async ({ }: DeleteRuntimeFieldArgs) => { usageCollection?.incrementCounter({ counterName }); const dataView = await dataViewsService.get(id); - const field = dataView.fields.getByName(name); + const field = dataView.getRuntimeField(name); if (!field) { throw new ErrorIndexPatternFieldNotFound(id, name); } - if (!field.runtimeField) { - throw new Error('Only runtime fields can be deleted.'); - } - dataView.removeRuntimeField(name); await dataViewsService.updateSavedObject(dataView); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.test.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.test.ts index 4e75e7b0d6f2c5..2552e88d5725ad 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.test.ts @@ -23,6 +23,8 @@ describe('get runtime field', () => { runtimeField: {}, }), }, + getRuntimeField: jest.fn().mockReturnValueOnce({}), + getFieldsByRuntimeFieldName: jest.fn().mockReturnValueOnce({}), } as unknown as DataView) ); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts index bcb9e931526266..b8f87a21b819fe 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts @@ -43,17 +43,13 @@ export const getRuntimeField = async ({ usageCollection?.incrementCounter({ counterName }); const dataView = await dataViewsService.get(id); - const field = dataView.fields.getByName(name); + const field = dataView.getRuntimeField(name); if (!field) { throw new ErrorIndexPatternFieldNotFound(id, name); } - if (!field.runtimeField) { - throw new Error('Only runtime fields can be retrieved.'); - } - - return { dataView, field }; + return { dataView, fields: Object.values(dataView.getFieldsByRuntimeFieldName(name) || {}) }; }; const getRuntimeFieldRouteFactory = @@ -95,7 +91,7 @@ const getRuntimeFieldRouteFactory = const id = req.params.id; const name = req.params.name; - const { dataView, field } = await getRuntimeField({ + const { dataView, fields } = await getRuntimeField({ dataViewsService, usageCollection, counterName: `${req.route.method} ${path}`, @@ -103,7 +99,7 @@ const getRuntimeFieldRouteFactory = name, }); - return res.ok(responseFormatter({ serviceKey, dataView, field })); + return res.ok(responseFormatter({ serviceKey, dataView, fields: fields || [] })); }) ); }; diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts index cf6b05c378e14e..a50fe841331597 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts @@ -10,7 +10,7 @@ import { UsageCounter } from 'src/plugins/usage_collection/server'; import { DataViewsService, RuntimeField } from 'src/plugins/data_views/common'; import { schema } from '@kbn/config-schema'; import { handleErrors } from '../util/handle_errors'; -import { runtimeFieldSpecSchema } from '../util/schemas'; +import { runtimeFieldSchema } from '../util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataViewsServerPluginStart, @@ -55,14 +55,11 @@ export const putRuntimeField = async ({ dataView.removeRuntimeField(name); } - dataView.addRuntimeField(name, runtimeField); + const fields = dataView.addRuntimeField(name, runtimeField); await dataViewsService.updateSavedObject(dataView); - const field = dataView.fields.getByName(name); - if (!field) throw new Error(`Could not create a field [name = ${name}].`); - - return { dataView, field }; + return { dataView, fields }; }; const putRuntimeFieldRouteFactory = @@ -90,7 +87,7 @@ const putRuntimeFieldRouteFactory = minLength: 1, maxLength: 1_000, }), - runtimeField: runtimeFieldSpecSchema, + runtimeField: runtimeFieldSchema, }), }, }, @@ -104,9 +101,12 @@ const putRuntimeFieldRouteFactory = req ); const id = req.params.id; - const { name, runtimeField } = req.body; + const { name, runtimeField } = req.body as { + name: string; + runtimeField: RuntimeField; + }; - const { dataView, field } = await putRuntimeField({ + const { dataView, fields } = await putRuntimeField({ dataViewsService, id, name, @@ -115,7 +115,7 @@ const putRuntimeFieldRouteFactory = counterName: `${req.route.method} ${path}`, }); - return res.ok(responseFormatter({ serviceKey, dataView, field })); + return res.ok(responseFormatter({ serviceKey, dataView, fields })); }) ); }; diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.test.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.test.ts index a2db82466868b0..3152ac1be91c2e 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.test.ts @@ -18,20 +18,22 @@ const dataView = { }, } as DataView; -const field = { - toSpec: () => { - return { - name: 'field', - }; +const fields = [ + { + toSpec: () => { + return { + name: 'field', + }; + }, }, -} as DataViewField; +] as DataViewField[]; describe('responseFormatter', () => { it('returns correct format', () => { const response = responseFormatter({ serviceKey: SERVICE_KEY, dataView, - field, + fields, }); expect(response).toMatchSnapshot(); }); @@ -40,7 +42,7 @@ describe('responseFormatter', () => { const response = responseFormatter({ serviceKey: SERVICE_KEY_LEGACY, dataView, - field, + fields, }); expect(response).toMatchSnapshot(); }); diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts index 77f431cef48f8d..7764f5bf0974b7 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts @@ -11,14 +11,14 @@ import { SERVICE_KEY_LEGACY, SERVICE_KEY_TYPE, SERVICE_KEY } from '../../constan interface ResponseFormatterArgs { serviceKey: SERVICE_KEY_TYPE; - field: DataViewField; + fields: DataViewField[]; dataView: DataView; } -export const responseFormatter = ({ serviceKey, field, dataView }: ResponseFormatterArgs) => { +export const responseFormatter = ({ serviceKey, fields, dataView }: ResponseFormatterArgs) => { const response = { body: { - fields: [field.toSpec()], + fields: fields.map((field) => field.toSpec()), [SERVICE_KEY]: dataView.toSpec(), }, }; @@ -26,7 +26,7 @@ export const responseFormatter = ({ serviceKey, field, dataView }: ResponseForma const legacyResponse = { body: { [SERVICE_KEY_LEGACY]: dataView.toSpec(), - field: field.toSpec(), + field: fields[0].toSpec(), }, }; diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.test.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.test.ts index e21c0775f79b24..f3ca214b41d0ff 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.test.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.test.ts @@ -22,7 +22,7 @@ describe('update runtime field', () => { addRuntimeField: jest.fn(), getRuntimeField: jest.fn().mockReturnValue({}), fields: { - getByName: jest.fn().mockReturnValue({ + getByName: jest.fn().mockReturnValueOnce({ runtimeField: {}, }), }, diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts index d097408ec211d3..27893ff1b7c05e 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts @@ -11,7 +11,7 @@ import { schema } from '@kbn/config-schema'; import { DataViewsService, RuntimeField } from 'src/plugins/data_views/common'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; -import { runtimeFieldSpec, runtimeFieldSpecTypeSchema } from '../util/schemas'; +import { runtimeFieldSchema } from '../util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataViewsServerPluginStart, @@ -52,16 +52,14 @@ export const updateRuntimeField = async ({ } dataView.removeRuntimeField(name); - dataView.addRuntimeField(name, { + const fields = dataView.addRuntimeField(name, { ...existingRuntimeField, ...runtimeField, }); await dataViewsService.updateSavedObject(dataView); - const field = dataView.fields.getByName(name); - if (!field) throw new Error(`Could not create a field [name = ${name}].`); - return { dataView, field }; + return { dataView, fields }; }; const updateRuntimeFieldRouteFactory = @@ -90,12 +88,7 @@ const updateRuntimeFieldRouteFactory = }), body: schema.object({ name: schema.never(), - runtimeField: schema.object({ - ...runtimeFieldSpec, - // We need to overwrite the below fields on top of `runtimeFieldSpec`, - // because some fields would be optional - type: schema.maybe(runtimeFieldSpecTypeSchema), - }), + runtimeField: runtimeFieldSchema, }), }, }, @@ -112,7 +105,7 @@ const updateRuntimeFieldRouteFactory = const name = req.params.name; const runtimeField = req.body.runtimeField as Partial; - const { dataView, field } = await updateRuntimeField({ + const { dataView, fields } = await updateRuntimeField({ dataViewsService, usageCollection, counterName: `${req.route.method} ${path}`, @@ -121,7 +114,7 @@ const updateRuntimeFieldRouteFactory = runtimeField, }); - return res.ok(responseFormatter({ serviceKey, dataView, field })); + return res.ok(responseFormatter({ serviceKey, dataView, fields })); }) ); }; diff --git a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts index ed3f75599b90a4..e71ae8c8ec88e0 100644 --- a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts +++ b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts @@ -10,11 +10,7 @@ import { schema } from '@kbn/config-schema'; import { DataViewSpec, DataViewsService } from 'src/plugins/data_views/common'; import { UsageCounter } from 'src/plugins/usage_collection/server'; import { handleErrors } from './util/handle_errors'; -import { - fieldSpecSchema, - runtimeFieldSpecSchema, - serializedFieldFormatSchema, -} from './util/schemas'; +import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../core/server'; import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; import { @@ -39,7 +35,7 @@ const indexPatternUpdateSchema = schema.object({ fieldFormats: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)), fields: schema.maybe(schema.recordOf(schema.string(), fieldSpecSchema)), allowNoIndex: schema.maybe(schema.boolean()), - runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSchema)), }); interface UpdateDataViewArgs { diff --git a/src/plugins/data_views/server/rest_api_routes/util/schemas.ts b/src/plugins/data_views/server/rest_api_routes/util/schemas.ts index 79f493f3038011..e31a4314c76f3c 100644 --- a/src/plugins/data_views/server/rest_api_routes/util/schemas.ts +++ b/src/plugins/data_views/server/rest_api_routes/util/schemas.ts @@ -7,7 +7,7 @@ */ import { schema, Type } from '@kbn/config-schema'; -import { RUNTIME_FIELD_TYPES, RuntimeType } from '../../../common'; +import { /* RUNTIME_FIELD_TYPES,*/ RuntimeType } from '../../../common'; export const serializedFieldFormatSchema = schema.object({ id: schema.maybe(schema.string()), @@ -60,17 +60,63 @@ export const fieldSpecSchema = schema.object(fieldSpecSchemaFields, { unknowns: 'ignore', }); -export const runtimeFieldSpecTypeSchema = schema.oneOf( - RUNTIME_FIELD_TYPES.map((runtimeFieldType) => schema.literal(runtimeFieldType)) as [ +export const RUNTIME_FIELD_TYPES2 = [ + 'keyword', + 'long', + 'double', + 'date', + 'ip', + 'boolean', + 'geo_point', +] as const; + +export const runtimeFieldNonCompositeFieldsSpecTypeSchema = schema.oneOf( + RUNTIME_FIELD_TYPES2.map((runtimeFieldType) => schema.literal(runtimeFieldType)) as [ Type ] ); -export const runtimeFieldSpec = { - type: runtimeFieldSpecTypeSchema, + +export const primitiveRuntimeFieldSchema = schema.object({ + type: runtimeFieldNonCompositeFieldsSpecTypeSchema, script: schema.maybe( schema.object({ source: schema.string(), }) ), -}; -export const runtimeFieldSpecSchema = schema.object(runtimeFieldSpec); + format: schema.maybe(serializedFieldFormatSchema), + customLabel: schema.maybe(schema.string()), + popularity: schema.maybe( + schema.number({ + min: 0, + }) + ), +}); + +export const compositeRuntimeFieldSchema = schema.object({ + type: schema.literal('composite') as Type, + script: schema.maybe( + schema.object({ + source: schema.string(), + }) + ), + fields: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + type: runtimeFieldNonCompositeFieldsSpecTypeSchema, + format: schema.maybe(serializedFieldFormatSchema), + customLabel: schema.maybe(schema.string()), + popularity: schema.maybe( + schema.number({ + min: 0, + }) + ), + }) + ) + ), +}); + +export const runtimeFieldSchema = schema.oneOf([ + primitiveRuntimeFieldSchema, + compositeRuntimeFieldSchema, +]); diff --git a/src/plugins/data_views/server/saved_objects/data_views.ts b/src/plugins/data_views/server/saved_objects/data_views.ts index ca7592732c3eed..972b8f5d6752a2 100644 --- a/src/plugins/data_views/server/saved_objects/data_views.ts +++ b/src/plugins/data_views/server/saved_objects/data_views.ts @@ -13,7 +13,7 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; export const dataViewSavedObjectType: SavedObjectsType = { name: DATA_VIEW_SAVED_OBJECT_TYPE, hidden: false, - namespaceType: 'multiple-isolated', + namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.0.0', management: { displayName: 'Data view', diff --git a/src/plugins/data_views/server/saved_objects_client_wrapper.ts b/src/plugins/data_views/server/saved_objects_client_wrapper.ts index dc7163c405d4f0..e00eb9e3375a82 100644 --- a/src/plugins/data_views/server/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/server/saved_objects_client_wrapper.ts @@ -42,6 +42,6 @@ export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommo return await this.savedObjectClient.create(type, attributes, options); } delete(type: string, id: string) { - return this.savedObjectClient.delete(type, id); + return this.savedObjectClient.delete(type, id, { force: true }); } } diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index f93bc2b49fdd51..8d2a6b2c048155 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -12,7 +12,6 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { esFilters } from '../../../../data/public'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { ContextErrorMessage } from './components/context_error_message'; import { DataView, DataViewField } from '../../../../data/common'; @@ -26,6 +25,7 @@ import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { generateFilters } from '../../../../data/public'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -111,13 +111,7 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { const addFilter = useCallback( async (field: DataViewField | string, values: unknown, operation: string) => { - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - indexPattern.id! - ); + const newFilters = generateFilters(filterManager, field, values, operation, indexPattern.id!); filterManager.addFilters(newFilters); if (indexPatterns) { const fieldName = typeof field === 'string' ? field : field.name; diff --git a/src/plugins/discover/public/application/context/services/context.ts b/src/plugins/discover/public/application/context/services/context.ts index d5dae272689b9e..5425c0448dca34 100644 --- a/src/plugins/discover/public/application/context/services/context.ts +++ b/src/plugins/discover/public/application/context/services/context.ts @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Filter, DataView, ISearchSource } from 'src/plugins/data/common'; +import type { Filter } from '@kbn/es-query'; +import { DataView, ISearchSource } from 'src/plugins/data/common'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from '../utils/sorting'; import { convertIsoToMillis, extractNanos } from '../utils/date_conversion'; diff --git a/src/plugins/discover/public/application/context/services/context_state.ts b/src/plugins/discover/public/application/context/services/context_state.ts index 8fb79f6f011c74..25d4d5e22126b2 100644 --- a/src/plugins/discover/public/application/context/services/context_state.ts +++ b/src/plugins/discover/public/application/context/services/context_state.ts @@ -9,6 +9,7 @@ import { isEqual } from 'lodash'; import { History } from 'history'; import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; +import { Filter, compareFilters, COMPARE_ALL_OPTIONS } from '@kbn/es-query'; import { createStateContainer, createKbnUrlStateStorage, @@ -16,7 +17,8 @@ import { withNotifyOnErrors, ReduxLikeStateContainer, } from '../../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../data/public'; + +import { FilterManager } from '../../../../../data/public'; import { handleSourceColumnState } from '../../../utils/state_helpers'; export interface AppState { @@ -222,7 +224,7 @@ export function isEqualFilters(filtersA: Filter[], filtersB: Filter[]) { } else if (!filtersA || !filtersB) { return false; } - return esFilters.compareFilters(filtersA, filtersB, esFilters.COMPARE_ALL_OPTIONS); + return compareFilters(filtersA, filtersB, COMPARE_ALL_OPTIONS); } /** @@ -239,7 +241,7 @@ function isEqualState(stateA: AppState | GlobalState, stateB: AppState | GlobalS const { filters: stateBFilters = [], ...stateBPartial } = stateB; return ( isEqual(stateAPartial, stateBPartial) && - esFilters.compareFilters(stateAFilters, stateBFilters, esFilters.COMPARE_ALL_OPTIONS) + compareFilters(stateAFilters, stateBFilters, COMPARE_ALL_OPTIONS) ); } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index aa2da1b2605fc3..765ea8f6f904d4 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; -import { esFilters } from '../../../../../../data/public'; +import { generateFilters } from '../../../../../../data/public'; import { DataViewField } from '../../../../../../data/common'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; @@ -172,7 +172,7 @@ export function DiscoverLayout({ (field: DataViewField | string, values: string, operation: '+' | '-') => { const fieldName = typeof field === 'string' ? field : field.name; popularizeField(indexPattern, fieldName, indexPatterns, capabilities); - const newFilters = esFilters.generateFilters( + const newFilters = generateFilters( filterManager, field, values, diff --git a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 059c247c38c2e0..a08b8f4022745a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -654,6 +654,7 @@ exports[`Discover DataView Management renders correctly 1`] = ` "_type", "_source", ], + "namespaces": Array [], "originalSavedObjectBody": Object {}, "resetOriginalSavedObjectBody": [Function], "runtimeFieldMap": Object {}, diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index ad448702aba402..4a3592f848de7e 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -10,6 +10,7 @@ import { isEqual, cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; +import { Filter, FilterStateStore, compareFilters, COMPARE_ALL_OPTIONS } from '@kbn/es-query'; import { createKbnUrlStateStorage, createStateContainer, @@ -22,8 +23,6 @@ import { import { connectToQueryState, DataPublicPluginStart, - esFilters, - Filter, FilterManager, Query, SearchSessionInfoProvider, @@ -277,7 +276,7 @@ export function getState({ data.query, appStateContainer, { - filters: esFilters.FilterStateStore.APP_STATE, + filters: FilterStateStore.APP_STATE, query: true, } ); @@ -322,7 +321,7 @@ export function isEqualFilters(filtersA: Filter[], filtersB: Filter[]) { } else if (!filtersA || !filtersB) { return false; } - return esFilters.compareFilters(filtersA, filtersB, esFilters.COMPARE_ALL_OPTIONS); + return compareFilters(filtersA, filtersB, COMPARE_ALL_OPTIONS); } /** diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 921ed32c0f1593..aa6b736a04ccb4 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -18,7 +18,12 @@ import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; import { SavedSearch } from '../services/saved_searches'; import { Adapters, RequestAdapter } from '../../../inspector/common'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; -import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../data/public'; +import { + APPLY_FILTER_TRIGGER, + esFilters, + FilterManager, + generateFilters, +} from '../../../data/public'; import { DiscoverServices } from '../build_services'; import { Filter, @@ -27,6 +32,7 @@ import { ISearchSource, Query, TimeRange, + FilterStateStore, } from '../../../data/common'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; import { UiActionsStart } from '../../../ui_actions/public'; @@ -281,7 +287,7 @@ export class SavedSearchEmbeddable }, sampleSize: 500, onFilter: async (field, value, operator) => { - let filters = esFilters.generateFilters( + let filters = generateFilters( this.filterManager, // @ts-expect-error field, @@ -291,7 +297,7 @@ export class SavedSearchEmbeddable ); filters = filters.map((filter) => ({ ...filter, - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, })); await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { diff --git a/src/plugins/discover/public/embeddable/types.ts b/src/plugins/discover/public/embeddable/types.ts index 32c029cccf7b12..95235659d27a81 100644 --- a/src/plugins/discover/public/embeddable/types.ts +++ b/src/plugins/discover/public/embeddable/types.ts @@ -12,7 +12,8 @@ import { EmbeddableOutput, IEmbeddable, } from 'src/plugins/embeddable/public'; -import { Filter, DataView, TimeRange, Query } from '../../../data/common'; +import type { Filter } from '@kbn/es-query'; +import { DataView, TimeRange, Query } from '../../../data/common'; import { SavedSearch } from '../services/saved_searches'; import { SortOrder } from '../components/doc_table/components/table_header/helpers'; diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index 569d31664eddbc..38b93f915114f1 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -7,9 +7,9 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; -import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import type { Filter } from '@kbn/es-query'; +import type { TimeRange, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; -import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import type { VIEW_MODE } from './components/view_mode_toggle'; @@ -127,10 +127,10 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition !esFilters.isFilterPinned(f)); + if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; if (columns) appState.columns = columns; if (savedQuery) appState.savedQuery = savedQuery; @@ -138,8 +138,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition esFilters.isFilterPinned(f)); + if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; if (viewMode) appState.viewMode = viewMode; if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index aacbc1b58f3e94..09042fda9a38ab 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; import { AppMountParameters, AppUpdater, @@ -18,8 +17,8 @@ import { Plugin, PluginInitializerContext, } from 'kibana/public'; -import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { SharePluginStart, SharePluginSetup } from 'src/plugins/share/public'; @@ -27,27 +26,24 @@ import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwardi import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { EuiLoadingContent } from '@elastic/eui'; -import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { SavedObjectsStart } from '../../saved_objects/public'; -import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; import { setDocViewsRegistry, - setUrlTracker, setHeaderActionMenuMounter, - setUiActions, setScopedHistory, - getScopedHistory, + setUiActions, + setUrlTracker, syncHistoryLocations, } from './kibana_services'; import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; -import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator'; +import { DiscoverAppLocator, DiscoverAppLocatorDefinition } from './locator'; import { SearchEmbeddableFactory } from './embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; -import { replaceUrlHashQuery } from '../../kibana_utils/public/'; import { IndexPatternFieldEditorStart } from '../../../plugins/data_view_field_editor/public'; import { DeferredSpinner } from './components'; import { ViewSavedSearchAction } from './embeddable/view_saved_search_action'; @@ -57,7 +53,7 @@ import { injectTruncateStyles } from './utils/truncate_styles'; import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common'; import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; import { useDiscoverServices } from './utils/use_discover_services'; -import { SEARCH_SESSION_ID_QUERY_PARAM } from './constants'; +import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking'; const DocViewerLegacyTable = React.lazy( () => import('./services/doc_views/components/doc_viewer_table/legacy') @@ -251,50 +247,8 @@ export class DiscoverPlugin ), }); - const { - appMounted, - appUnMounted, - stop: stopUrlTracker, - setActiveUrl: setTrackedUrl, - restorePreviousUrl, - } = createKbnUrlTracker({ - // we pass getter here instead of plain `history`, - // so history is lazily created (when app is mounted) - // this prevents redundant `#` when not in discover app - getHistory: getScopedHistory, - baseUrl, - defaultSubUrl: '#/', - storageKey: `lastUrl:${core.http.basePath.get()}:discover`, - navLinkUpdater$: this.appStateUpdater, - toastNotifications: core.notifications.toasts, - stateParams: [ - { - kbnUrlKey: '_g', - stateUpdate$: plugins.data.query.state$.pipe( - filter( - ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) - ), - }, - ], - onBeforeNavLinkSaved: (newNavLink: string) => { - // Do not save SEARCH_SESSION_ID into nav link, because of possible edge cases - // that could lead to session restoration failure. - // see: https://github.com/elastic/kibana/issues/87149 - if (newNavLink.includes(SEARCH_SESSION_ID_QUERY_PARAM)) { - newNavLink = replaceUrlHashQuery(newNavLink, (query) => { - delete query[SEARCH_SESSION_ID_QUERY_PARAM]; - return query; - }); - } - - return newNavLink; - }, - }); + const { setTrackedUrl, restorePreviousUrl, stopUrlTracker, appMounted, appUnMounted } = + initializeKbnUrlTracking(baseUrl, core, this.appStateUpdater, plugins); setUrlTracker({ setTrackedUrl, restorePreviousUrl }); this.stopUrlTracking = () => { stopUrlTracker(); @@ -309,6 +263,7 @@ export class DiscoverPlugin defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { + const [coreStart, discoverStartPlugins] = await core.getStartServices(); setScopedHistory(params.history); setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); @@ -319,7 +274,6 @@ export class DiscoverPlugin window.dispatchEvent(new HashChangeEvent('hashchange')); }); - const [coreStart, discoverStartPlugins] = await core.getStartServices(); const services = buildServices(coreStart, discoverStartPlugins, this.initializerContext); // make sure the index pattern list is up to date diff --git a/src/plugins/discover/public/utils/get_sharing_data.ts b/src/plugins/discover/public/utils/get_sharing_data.ts index cd00fc5e3c70e1..deb8d0904782d3 100644 --- a/src/plugins/discover/public/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.ts @@ -9,7 +9,8 @@ import type { Capabilities } from 'kibana/public'; import type { IUiSettingsClient } from 'kibana/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; -import type { Filter, ISearchSource, SerializedSearchSourceFields } from 'src/plugins/data/common'; +import type { Filter } from '@kbn/es-query'; +import type { ISearchSource, SerializedSearchSourceFields } from 'src/plugins/data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SEARCH_FIELDS_FROM_SOURCE, diff --git a/src/plugins/discover/public/utils/initialize_kbn_url_tracking.test.ts b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.test.ts new file mode 100644 index 00000000000000..a3a373adbbf028 --- /dev/null +++ b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { AppUpdater } from 'kibana/public'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { coreMock } from 'src/core/public/mocks'; +import { DiscoverSetupPlugins } from '../plugin'; +import { initializeKbnUrlTracking } from './initialize_kbn_url_tracking'; + +describe('initializeKbnUrlTracking', () => { + test('returns functions to start and stop url tracking', async () => { + const pluginsSetup = { + data: { + query: { + state$: new Observable(), + }, + }, + } as DiscoverSetupPlugins; + const result = initializeKbnUrlTracking( + '', + coreMock.createSetup(), + new BehaviorSubject(() => ({})), + pluginsSetup + ); + expect(result).toMatchInlineSnapshot(` + Object { + "appMounted": [Function], + "appUnMounted": [Function], + "restorePreviousUrl": [Function], + "setTrackedUrl": [Function], + "stopUrlTracker": [Function], + } + `); + }); +}); diff --git a/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.ts new file mode 100644 index 00000000000000..139f3b5a0c6314 --- /dev/null +++ b/src/plugins/discover/public/utils/initialize_kbn_url_tracking.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { AppUpdater, CoreSetup } from 'kibana/public'; +import type { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { createKbnUrlTracker, replaceUrlHashQuery } from '../../../kibana_utils/public'; +import { getScopedHistory } from '../kibana_services'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../constants'; +import type { DiscoverSetupPlugins } from '../plugin'; + +/** + * It creates the kbn url tracker for Discover to listens to history changes and optionally to global state + * changes and updates the nav link url of to point to the last visited page + */ +export function initializeKbnUrlTracking( + baseUrl: string, + core: CoreSetup, + navLinkUpdater$: BehaviorSubject, + plugins: DiscoverSetupPlugins +) { + const { + appMounted, + appUnMounted, + stop: stopUrlTracker, + setActiveUrl: setTrackedUrl, + restorePreviousUrl, + } = createKbnUrlTracker({ + // we pass getter here instead of plain `history`, + // so history is lazily created (when app is mounted) + // this prevents redundant `#` when not in discover app + getHistory: getScopedHistory, + baseUrl, + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:discover`, + navLinkUpdater$, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: plugins.data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(async ({ state }) => { + const { isFilterPinned } = await import('@kbn/es-query'); + return { + ...state, + filters: state.filters?.filter(isFilterPinned), + }; + }) + ), + }, + ], + onBeforeNavLinkSaved: (newNavLink: string) => { + // Do not save SEARCH_SESSION_ID into nav link, because of possible edge cases + // that could lead to session restoration failure. + // see: https://github.com/elastic/kibana/issues/87149 + if (newNavLink.includes(SEARCH_SESSION_ID_QUERY_PARAM)) { + newNavLink = replaceUrlHashQuery(newNavLink, (query) => { + delete query[SEARCH_SESSION_ID_QUERY_PARAM]; + return query; + }); + } + + return newNavLink; + }, + }); + return { + appMounted, + appUnMounted, + stopUrlTracker, + setTrackedUrl, + restorePreviousUrl, + }; +} diff --git a/src/plugins/discover/public/utils/use_navigation_props.tsx b/src/plugins/discover/public/utils/use_navigation_props.tsx index 963499c4cad57a..74df39e09e41b0 100644 --- a/src/plugins/discover/public/utils/use_navigation_props.tsx +++ b/src/plugins/discover/public/utils/use_navigation_props.tsx @@ -11,7 +11,8 @@ import { useHistory, matchPath } from 'react-router-dom'; import type { Location } from 'history'; import { stringify } from 'query-string'; import rison from 'rison-node'; -import { esFilters, FilterManager } from '../../../data/public'; +import { disableFilter } from '@kbn/es-query'; +import { FilterManager } from '../../../data/public'; import { url } from '../../../kibana_utils/common'; import { useDiscoverServices } from './use_discover_services'; @@ -39,7 +40,7 @@ export const getContextHash = (columns: string[], filterManager: FilterManager) }), _a: rison.encode({ columns, - filters: (appFilters || []).map(esFilters.disableFilter), + filters: (appFilters || []).map(disableFilter), }), }), { encode: false, sort: false } diff --git a/src/plugins/saved_objects_management/public/services/column_service.test.ts b/src/plugins/saved_objects_management/public/services/column_service.test.ts index 581a55fa0066db..5676cfaa812851 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.test.ts @@ -7,7 +7,7 @@ */ import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; -// import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; +import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; import { SavedObjectsManagementColumnService, SavedObjectsManagementColumnServiceSetup, @@ -45,7 +45,7 @@ describe('SavedObjectsManagementColumnRegistry', () => { const start = service.start(spacesPluginMock.createStartContract()); expect(start.getAll()).toEqual([ column, - // expect.any(ShareToSpaceSavedObjectsManagementColumn), + expect.any(ShareToSpaceSavedObjectsManagementColumn), ]); }); diff --git a/src/plugins/saved_objects_management/public/services/column_service.ts b/src/plugins/saved_objects_management/public/services/column_service.ts index 74c06a3d33218c..8189fc7d07f83c 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.ts @@ -7,7 +7,7 @@ */ import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; -// import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; +import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; import { SavedObjectsManagementColumn } from './types'; export interface SavedObjectsManagementColumnServiceSetup { @@ -53,5 +53,5 @@ function registerSpacesApiColumns( spacesApi: SpacesApi ) { // Note: this column is hidden for now because no saved objects are shareable. It should be uncommented when at least one saved object type is multi-namespace. - // service.setup().register(new ShareToSpaceSavedObjectsManagementColumn(spacesApi.ui)); + service.setup().register(new ShareToSpaceSavedObjectsManagementColumn(spacesApi.ui)); } diff --git a/src/plugins/shared_ux/kibana.json b/src/plugins/shared_ux/kibana.json index 44aeeb9cf80fcd..308a252f70b549 100755 --- a/src/plugins/shared_ux/kibana.json +++ b/src/plugins/shared_ux/kibana.json @@ -9,6 +9,6 @@ "description": "A plugin providing components and services for shared user experiences in Kibana.", "server": true, "ui": true, - "requiredPlugins": [], + "requiredPlugins": ["dataViewEditor", "dataViews"], "optionalPlugins": [] } diff --git a/src/plugins/shared_ux/public/components/empty_state/assets/data_view_illustration.tsx b/src/plugins/shared_ux/public/components/empty_state/assets/data_view_illustration.tsx new file mode 100644 index 00000000000000..8a889a9267dee4 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/assets/data_view_illustration.tsx @@ -0,0 +1,552 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export const DataViewIllustration = () => { + const { euiTheme } = useEuiTheme(); + const { colors } = euiTheme; + + const dataViewIllustrationVerticalStripes = css` + fill: ${colors.fullShade}; + `; + + const dataViewIllustrationDots = css` + fill: ${colors.lightShade}; + `; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/shared_ux/public/components/empty_state/assets/index.tsx b/src/plugins/shared_ux/public/components/empty_state/assets/index.tsx new file mode 100644 index 00000000000000..c234cdf40055b7 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/assets/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { withSuspense } from '../../utility'; + +export const LazyDataViewIllustration = React.lazy(() => + import('../assets/data_view_illustration').then(({ DataViewIllustration }) => ({ + default: DataViewIllustration, + })) +); + +export const DataViewIllustration = withSuspense( + LazyDataViewIllustration, + +); diff --git a/src/plugins/shared_ux/public/components/empty_state/index.tsx b/src/plugins/shared_ux/public/components/empty_state/index.tsx new file mode 100644 index 00000000000000..fc05199aae2073 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NoDataViews } from './no_data_views'; diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap b/src/plugins/shared_ux/public/components/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap new file mode 100644 index 00000000000000..5526b78bceb730 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/__snapshots__/documentation_link.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` is rendered correctly 1`] = ` +
+ +
+ +
+
+   +
+ + + +
+
+`; diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.test.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.test.tsx new file mode 100644 index 00000000000000..fd963dfaa8e1cf --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { EuiLink, EuiTitle } from '@elastic/eui'; +import { shallowWithIntl } from '@kbn/test-jest-helpers'; + +import { DocumentationLink } from './documentation_link'; + +describe('', () => { + test('is rendered correctly', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + + expect(component.find('dl').length).toBe(1); + expect(component.find(EuiTitle).length).toBe(1); + expect(component.find(EuiLink).length).toBe(1); + + const link = component.find(EuiLink).at(0); + expect(link.prop('href')).toBe('dummy'); + }); +}); diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.tsx new file mode 100644 index 00000000000000..4ac07899fef5fe --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/documentation_link.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiLink, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + href: string; +} + +export function DocumentationLink({ href }: Props) { + return ( +
+ +
+ +
+
+   +
+ + + +
+
+ ); +} diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/index.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/index.tsx new file mode 100644 index 00000000000000..fc05199aae2073 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NoDataViews } from './no_data_views'; diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.test.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.test.tsx new file mode 100644 index 00000000000000..1d8028d4889a04 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiButton, EuiPanel } from '@elastic/eui'; +import { NoDataViews } from './no_data_views.component'; +import { DocumentationLink } from './documentation_link'; + +describe('', () => { + test('is rendered correctly', () => { + const component = mountWithIntl( + + ); + expect(component.find(EuiPanel).length).toBe(1); + expect(component.find(EuiButton).length).toBe(1); + expect(component.find(DocumentationLink).length).toBe(1); + }); + + test('does not render button if canCreateNewDataViews is false', () => { + const component = mountWithIntl(); + + expect(component.find(EuiButton).length).toBe(0); + }); + + test('does not documentation link if linkToDocumentation is not provided', () => { + const component = mountWithIntl( + + ); + + expect(component.find(DocumentationLink).length).toBe(0); + }); + + test('onClickCreate', () => { + const onClickCreate = jest.fn(); + const component = mountWithIntl( + + ); + + component.find('button').simulate('click'); + + expect(onClickCreate).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.tsx new file mode 100644 index 00000000000000..bfab91ef03b264 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.component.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { css } from '@emotion/react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; + +import { DataViewIllustration } from '../assets'; +import { DocumentationLink } from './documentation_link'; + +export interface Props { + canCreateNewDataView: boolean; + onClickCreate?: () => void; + dataViewsDocLink?: string; + emptyPromptColor?: EuiEmptyPromptProps['color']; +} + +const createDataViewText = i18n.translate('sharedUX.noDataViewsPage.addDataViewText', { + defaultMessage: 'Create Data View', +}); + +// Using raw value because it is content dependent +const MAX_WIDTH = 830; + +/** + * A presentational component that is shown in cases when there are no data views created yet. + */ +export const NoDataViews = ({ + onClickCreate, + canCreateNewDataView, + dataViewsDocLink, + emptyPromptColor = 'plain', +}: Props) => { + const createNewButton = canCreateNewDataView && ( + + {createDataViewText} + + ); + + return ( + } + title={ +

+ +
+ +

+ } + body={ +

+ +

+ } + actions={createNewButton} + footer={dataViewsDocLink && } + /> + ); +}; diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.mdx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.mdx new file mode 100644 index 00000000000000..ef8812c565a9f3 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.mdx @@ -0,0 +1,14 @@ +**id:** sharedUX/Components/NoDataViewsPage +**slug:** /shared-ux/components/no-data-views-page +**title:** No Data Views Page +**summary:** A page to be displayed when there is data in Elasticsearch, but no data views +**tags:** ['shared-ux', 'component'] +**date:** 2022-02-09 + +--- + +When there is data in Elasticsearch, but there haven't been any data views created yet, we want to display an appropriate message to the user and facilitate creation of data views (if appropriate permissions are in place). + +The pure component, `no_data_views.component.tsx`, is a pre-configured **EuiEmptyPrompt**. + +The connected component, `no_data_views.tsx`, uses services from the `shared_ux` plugin to open Data View Editor. You must wrap your plugin app in the `ServicesContext` provided by the start contract of the `shared_ux` plugin to use it. diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.stories.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.stories.tsx new file mode 100644 index 00000000000000..3f9ae1958cad7f --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.stories.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { docLinksServiceFactory } from '../../../services/storybook/doc_links'; + +import { NoDataViews as NoDataViewsComponent, Props } from './no_data_views.component'; +import { NoDataViews } from './no_data_views'; + +import mdx from './no_data_views.mdx'; + +export default { + title: 'No Data Views', + description: 'A component to display when there are no user-created data views available.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const ConnectedComponent = () => { + return ( + + ); +}; + +type Params = Pick; + +export const PureComponent = (params: Params) => { + return ; +}; + +PureComponent.argTypes = { + canCreateNewDataView: { + control: 'boolean', + defaultValue: true, + }, + dataViewsDocLink: { + options: [docLinksServiceFactory().dataViewsDocsLink, undefined], + control: { type: 'radio' }, + }, +}; diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.test.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.test.tsx new file mode 100644 index 00000000000000..650d78fa64a036 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { ReactWrapper } from 'enzyme'; + +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiButton } from '@elastic/eui'; + +import { ServicesProvider, SharedUXServices } from '../../../services'; +import { servicesFactory } from '../../../services/mocks'; +import { NoDataViews } from './no_data_views'; + +describe('', () => { + let services: SharedUXServices; + let mount: (element: JSX.Element) => ReactWrapper; + + beforeEach(() => { + services = servicesFactory(); + mount = (element: JSX.Element) => + mountWithIntl({element}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('on dataView created', () => { + const component = mount( + + ); + + expect(services.editors.openDataViewEditor).not.toHaveBeenCalled(); + component.find(EuiButton).simulate('click'); + + component.unmount(); + + expect(services.editors.openDataViewEditor).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.tsx b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.tsx new file mode 100644 index 00000000000000..01494d2c8a5b31 --- /dev/null +++ b/src/plugins/shared_ux/public/components/empty_state/no_data_views/no_data_views.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useRef } from 'react'; + +import { DataView } from '../../../../../data_views/public'; +import { useEditors, usePermissions } from '../../../services'; +import type { SharedUXEditorsService } from '../../../services/editors'; + +import { NoDataViews as NoDataViewsComponent } from './no_data_views.component'; + +export interface Props { + onDataViewCreated: (dataView: DataView) => void; + dataViewsDocLink: string; +} + +type CloseDataViewEditorFn = ReturnType; + +/** + * A service-enabled component that provides Kibana-specific functionality to the `NoDataViews` + * component. + * + * Use of this component requires both the `EuiTheme` context as well as either a configured Shared UX + * `ServicesProvider` or the `ServicesContext` provided by the Shared UX public plugin contract. + * + * See shared-ux/public/services for information. + */ +export const NoDataViews = ({ onDataViewCreated, dataViewsDocLink }: Props) => { + const { canCreateNewDataView } = usePermissions(); + const { openDataViewEditor } = useEditors(); + const closeDataViewEditor = useRef(); + + useEffect(() => { + const cleanup = () => { + if (closeDataViewEditor?.current) { + closeDataViewEditor?.current(); + } + }; + + return () => { + // Make sure to close the editor when unmounting + cleanup(); + }; + }, []); + + const setDataViewEditorRef = useCallback((ref: CloseDataViewEditorFn) => { + closeDataViewEditor.current = ref; + }, []); + + const onClickCreate = useCallback(() => { + if (!canCreateNewDataView) { + return; + } + + const ref = openDataViewEditor({ + onSave: (dataView) => { + onDataViewCreated(dataView); + }, + }); + + if (setDataViewEditorRef) { + setDataViewEditorRef(ref); + } + }, [canCreateNewDataView, openDataViewEditor, setDataViewEditorRef, onDataViewCreated]); + + return ; +}; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap index abf76f9da48ce5..d2609e6b3c7a64 100644 --- a/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -2,6 +2,21 @@ exports[` is rendered 1`] = ` * a predefined fallback and error boundary. */ export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton); + +/** + * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the + * `withSuspense` HOC to load this component. + */ +export const LazyNoDataViewsPage = React.lazy(() => + import('./empty_state/no_data_views').then(({ NoDataViews }) => ({ + default: NoDataViews, + })) +); + +/** + * A `NoDataViewsPage` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `LazyNoDataViewsPage` component lazily with + * a predefined fallback and error boundary. + */ +export const NoDataViewsPage = withSuspense(LazyNoDataViewsPage); diff --git a/src/plugins/shared_ux/public/index.ts b/src/plugins/shared_ux/public/index.ts index e6a2f925c51206..a196a60db847b6 100755 --- a/src/plugins/shared_ux/public/index.ts +++ b/src/plugins/shared_ux/public/index.ts @@ -17,3 +17,4 @@ export function plugin() { export type { SharedUXPluginSetup, SharedUXPluginStart } from './types'; export { ExitFullScreenButton, LazyExitFullScreenButton } from './components'; +export { NoDataViewsPage, LazyNoDataViewsPage } from './components'; diff --git a/src/plugins/shared_ux/public/services/doc_links.ts b/src/plugins/shared_ux/public/services/doc_links.ts new file mode 100644 index 00000000000000..3c6d23bd33ae49 --- /dev/null +++ b/src/plugins/shared_ux/public/services/doc_links.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export interface SharedUXDocLinksService { + dataViewsDocsLink: string; +} diff --git a/src/plugins/shared_ux/public/services/editors.ts b/src/plugins/shared_ux/public/services/editors.ts new file mode 100644 index 00000000000000..176b22a6006e16 --- /dev/null +++ b/src/plugins/shared_ux/public/services/editors.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { DataView } from '../../../data_views/common'; + +export interface SharedUxDataViewEditorProps { + onSave: (dataView: DataView) => void; +} +export interface SharedUXEditorsService { + openDataViewEditor: (options: SharedUxDataViewEditorProps) => () => void; +} diff --git a/src/plugins/shared_ux/public/services/index.tsx b/src/plugins/shared_ux/public/services/index.tsx index 0677f3ef0ca84d..bdca90c725858c 100644 --- a/src/plugins/shared_ux/public/services/index.tsx +++ b/src/plugins/shared_ux/public/services/index.tsx @@ -9,6 +9,9 @@ import React, { FC, createContext, useContext } from 'react'; import { SharedUXPlatformService } from './platform'; import { servicesFactory } from './stub'; +import { SharedUXUserPermissionsService } from './permissions'; +import { SharedUXEditorsService } from './editors'; +import { SharedUXDocLinksService } from './doc_links'; /** * A collection of services utilized by SharedUX. This serves as a thin @@ -20,6 +23,9 @@ import { servicesFactory } from './stub'; */ export interface SharedUXServices { platform: SharedUXPlatformService; + permissions: SharedUXUserPermissionsService; + editors: SharedUXEditorsService; + docLinks: SharedUXDocLinksService; } // The React Context used to provide the services to the SharedUX components. @@ -48,3 +54,9 @@ export function useServices() { * React hook for accessing the pre-wired `SharedUXPlatformService`. */ export const usePlatformService = () => useServices().platform; + +export const usePermissions = () => useServices().permissions; + +export const useEditors = () => useServices().editors; + +export const useDocLinks = () => useServices().docLinks; diff --git a/src/plugins/shared_ux/public/services/kibana/doc_links.ts b/src/plugins/shared_ux/public/services/kibana/doc_links.ts new file mode 100644 index 00000000000000..eb25114a188a27 --- /dev/null +++ b/src/plugins/shared_ux/public/services/kibana/doc_links.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '../types'; +import { SharedUXPluginStartDeps } from '../../types'; +import { SharedUXDocLinksService } from '../doc_links'; + +export type DocLinksServiceFactory = KibanaPluginServiceFactory< + SharedUXDocLinksService, + SharedUXPluginStartDeps +>; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXEditorsService`. + */ +export const docLinksServiceFactory: DocLinksServiceFactory = ({ coreStart }) => ({ + dataViewsDocsLink: coreStart.docLinks.links.indexPatterns?.introduction, +}); diff --git a/src/plugins/shared_ux/public/services/kibana/editors.ts b/src/plugins/shared_ux/public/services/kibana/editors.ts new file mode 100644 index 00000000000000..0f8082d7d00e2a --- /dev/null +++ b/src/plugins/shared_ux/public/services/kibana/editors.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '../types'; +import { SharedUXEditorsService } from '../editors'; +import { SharedUXPluginStartDeps } from '../../types'; + +export type EditorsServiceFactory = KibanaPluginServiceFactory< + SharedUXEditorsService, + SharedUXPluginStartDeps +>; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXEditorsService`. + */ +export const editorsServiceFactory: EditorsServiceFactory = ({ startPlugins }) => ({ + openDataViewEditor: startPlugins.dataViewEditor.openEditor, +}); diff --git a/src/plugins/shared_ux/public/services/kibana/index.ts b/src/plugins/shared_ux/public/services/kibana/index.ts index f7c4cd7b2c56d7..506176cffbc458 100644 --- a/src/plugins/shared_ux/public/services/kibana/index.ts +++ b/src/plugins/shared_ux/public/services/kibana/index.ts @@ -10,6 +10,9 @@ import type { SharedUXServices } from '..'; import type { SharedUXPluginStartDeps } from '../../types'; import type { KibanaPluginServiceFactory } from '../types'; import { platformServiceFactory } from './platform'; +import { userPermissionsServiceFactory } from './permissions'; +import { editorsServiceFactory } from './editors'; +import { docLinksServiceFactory } from './doc_links'; /** * A factory function for creating a Kibana-based implementation of `SharedUXServices`. @@ -19,4 +22,7 @@ export const servicesFactory: KibanaPluginServiceFactory< SharedUXPluginStartDeps > = (params) => ({ platform: platformServiceFactory(params), + permissions: userPermissionsServiceFactory(params), + editors: editorsServiceFactory(params), + docLinks: docLinksServiceFactory(params), }); diff --git a/src/plugins/shared_ux/public/services/kibana/permissions.ts b/src/plugins/shared_ux/public/services/kibana/permissions.ts new file mode 100644 index 00000000000000..caaeccdccbccd5 --- /dev/null +++ b/src/plugins/shared_ux/public/services/kibana/permissions.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '../types'; +import { SharedUXPluginStartDeps } from '../../types'; +import { SharedUXUserPermissionsService } from '../permissions'; + +export type UserPermissionsServiceFactory = KibanaPluginServiceFactory< + SharedUXUserPermissionsService, + SharedUXPluginStartDeps +>; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXPermissionsService`. + */ +export const userPermissionsServiceFactory: UserPermissionsServiceFactory = ({ startPlugins }) => ({ + canCreateNewDataView: startPlugins.dataViewEditor.userPermissions.editDataView(), +}); diff --git a/src/plugins/shared_ux/public/services/mocks/doc_links.mock.ts b/src/plugins/shared_ux/public/services/mocks/doc_links.mock.ts new file mode 100644 index 00000000000000..28cfa14c50d287 --- /dev/null +++ b/src/plugins/shared_ux/public/services/mocks/doc_links.mock.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXDocLinksService } from '../doc_links'; + +export type MockDockLinksServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXDocLinksService`. + */ +export const docLinksServiceFactory: MockDockLinksServiceFactory = () => ({ + dataViewsDocsLink: 'dummy link', +}); diff --git a/src/plugins/shared_ux/public/services/mocks/editors.mock.ts b/src/plugins/shared_ux/public/services/mocks/editors.mock.ts new file mode 100644 index 00000000000000..28a89d5326d629 --- /dev/null +++ b/src/plugins/shared_ux/public/services/mocks/editors.mock.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXEditorsService } from '../editors'; + +export type MockEditorsServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXEditorsService`. + */ +export const editorsServiceFactory: MockEditorsServiceFactory = () => ({ + openDataViewEditor: jest.fn(), +}); diff --git a/src/plugins/shared_ux/public/services/mocks/index.ts b/src/plugins/shared_ux/public/services/mocks/index.ts index 14d38a484e53b3..9fce633c52539c 100644 --- a/src/plugins/shared_ux/public/services/mocks/index.ts +++ b/src/plugins/shared_ux/public/services/mocks/index.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { docLinksServiceFactory } from './doc_links.mock'; export type { MockPlatformServiceFactory } from './platform.mock'; export { platformServiceFactory } from './platform.mock'; @@ -12,10 +13,15 @@ export { platformServiceFactory } from './platform.mock'; import type { SharedUXServices } from '../.'; import { PluginServiceFactory } from '../types'; import { platformServiceFactory } from './platform.mock'; +import { userPermissionsServiceFactory } from './permissions.mock'; +import { editorsServiceFactory } from './editors.mock'; /** * A factory function for creating a Jest-based implementation of `SharedUXServices`. */ export const servicesFactory: PluginServiceFactory = () => ({ platform: platformServiceFactory(), + permissions: userPermissionsServiceFactory(), + editors: editorsServiceFactory(), + docLinks: docLinksServiceFactory(), }); diff --git a/src/plugins/shared_ux/public/services/mocks/permissions.mock.ts b/src/plugins/shared_ux/public/services/mocks/permissions.mock.ts new file mode 100644 index 00000000000000..ff65d2393248ab --- /dev/null +++ b/src/plugins/shared_ux/public/services/mocks/permissions.mock.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXUserPermissionsService } from '../permissions'; + +export type MockUserPermissionsServiceFactory = + PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXUserPermissionsService`. + */ +export const userPermissionsServiceFactory: MockUserPermissionsServiceFactory = () => ({ + canCreateNewDataView: true, +}); diff --git a/src/plugins/shared_ux/public/services/permissions.ts b/src/plugins/shared_ux/public/services/permissions.ts new file mode 100644 index 00000000000000..009f497e357063 --- /dev/null +++ b/src/plugins/shared_ux/public/services/permissions.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface SharedUXUserPermissionsService { + canCreateNewDataView: boolean; +} diff --git a/src/plugins/shared_ux/public/services/storybook/doc_links.ts b/src/plugins/shared_ux/public/services/storybook/doc_links.ts new file mode 100644 index 00000000000000..548d5043361083 --- /dev/null +++ b/src/plugins/shared_ux/public/services/storybook/doc_links.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXDocLinksService } from '../doc_links'; + +export type SharedUXDockLinksServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXDocLinksService`. + */ +export const docLinksServiceFactory: SharedUXDockLinksServiceFactory = () => ({ + dataViewsDocsLink: 'https://www.elastic.co/guide/en/kibana/master/data-views.html', +}); diff --git a/src/plugins/shared_ux/public/services/storybook/editors.ts b/src/plugins/shared_ux/public/services/storybook/editors.ts new file mode 100644 index 00000000000000..248699d5beb959 --- /dev/null +++ b/src/plugins/shared_ux/public/services/storybook/editors.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { action } from '@storybook/addon-actions'; +import { PluginServiceFactory } from '../types'; +import { SharedUxDataViewEditorProps, SharedUXEditorsService } from '../editors'; + +export type SharedUXEditorsServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a storybook implementation of `SharedUXEditorsService`. + */ +export const editorsServiceFactory: SharedUXEditorsServiceFactory = () => ({ + openDataViewEditor: action('openEditor') as SharedUXEditorsService['openDataViewEditor'] as ( + options: SharedUxDataViewEditorProps + ) => () => void, +}); diff --git a/src/plugins/shared_ux/public/services/storybook/index.ts b/src/plugins/shared_ux/public/services/storybook/index.ts index 8ea03317e53a28..c915b1a4633652 100644 --- a/src/plugins/shared_ux/public/services/storybook/index.ts +++ b/src/plugins/shared_ux/public/services/storybook/index.ts @@ -9,10 +9,16 @@ import type { SharedUXServices } from '../.'; import { PluginServiceFactory } from '../types'; import { platformServiceFactory } from './platform'; +import { editorsServiceFactory } from './editors'; +import { userPermissionsServiceFactory } from './permissions'; +import { docLinksServiceFactory } from './doc_links'; /** * A factory function for creating a Storybook-based implementation of `SharedUXServices`. */ export const servicesFactory: PluginServiceFactory = (params) => ({ platform: platformServiceFactory(params), + permissions: userPermissionsServiceFactory(), + editors: editorsServiceFactory(), + docLinks: docLinksServiceFactory(), }); diff --git a/src/plugins/shared_ux/public/services/storybook/permissions.ts b/src/plugins/shared_ux/public/services/storybook/permissions.ts new file mode 100644 index 00000000000000..c7110fccf7eeab --- /dev/null +++ b/src/plugins/shared_ux/public/services/storybook/permissions.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXUserPermissionsService } from '../permissions'; + +export type SharedUXUserPermissionsServiceFactory = + PluginServiceFactory; + +/** + * A factory function for creating a storybook implementation of `SharedUXUserPermissionsService`. + */ +export const userPermissionsServiceFactory: SharedUXUserPermissionsServiceFactory = () => ({ + canCreateNewDataView: true, +}); diff --git a/src/plugins/shared_ux/public/services/stub/doc_links.ts b/src/plugins/shared_ux/public/services/stub/doc_links.ts new file mode 100644 index 00000000000000..424a24c5785396 --- /dev/null +++ b/src/plugins/shared_ux/public/services/stub/doc_links.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXDocLinksService } from '../doc_links'; + +export type DockLinksServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXDocLinksService`. + */ +export const docLinksServiceFactory: DockLinksServiceFactory = () => ({ + dataViewsDocsLink: 'docs', +}); diff --git a/src/plugins/shared_ux/public/services/stub/editors.ts b/src/plugins/shared_ux/public/services/stub/editors.ts new file mode 100644 index 00000000000000..03fea5e6c98b58 --- /dev/null +++ b/src/plugins/shared_ux/public/services/stub/editors.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXEditorsService } from '../editors'; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXEditorsService`. + */ +export type EditorsServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXEditorsService`. + */ +export const editorsServiceFactory: EditorsServiceFactory = () => ({ + openDataViewEditor: () => () => {}, +}); diff --git a/src/plugins/shared_ux/public/services/stub/index.ts b/src/plugins/shared_ux/public/services/stub/index.ts index 8a5afe8cdcafbe..9e4fa8f03133aa 100644 --- a/src/plugins/shared_ux/public/services/stub/index.ts +++ b/src/plugins/shared_ux/public/services/stub/index.ts @@ -9,10 +9,16 @@ import type { SharedUXServices } from '../.'; import { PluginServiceFactory } from '../types'; import { platformServiceFactory } from './platform'; +import { userPermissionsServiceFactory } from './permissions'; +import { editorsServiceFactory } from './editors'; +import { docLinksServiceFactory } from './doc_links'; /** * A factory function for creating a simple stubbed implemetation of `SharedUXServices`. */ export const servicesFactory: PluginServiceFactory = () => ({ platform: platformServiceFactory(), + permissions: userPermissionsServiceFactory(), + editors: editorsServiceFactory(), + docLinks: docLinksServiceFactory(), }); diff --git a/src/plugins/shared_ux/public/services/stub/permissions.ts b/src/plugins/shared_ux/public/services/stub/permissions.ts new file mode 100644 index 00000000000000..c51abf41e28429 --- /dev/null +++ b/src/plugins/shared_ux/public/services/stub/permissions.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXUserPermissionsService } from '../permissions'; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXUserPermissionsService`. + */ +export type UserPermissionsServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXUserPermissionsService`. + */ +export const userPermissionsServiceFactory: UserPermissionsServiceFactory = () => ({ + canCreateNewDataView: true, +}); diff --git a/src/plugins/shared_ux/public/types/index.ts b/src/plugins/shared_ux/public/types/index.ts index 767dbf1aa10a47..38f91815f2e7b2 100644 --- a/src/plugins/shared_ux/public/types/index.ts +++ b/src/plugins/shared_ux/public/types/index.ts @@ -9,6 +9,7 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { FC } from 'react'; +import { DataViewEditorStart } from 'src/plugins/data_view_editor/public'; /** @internal */ export interface SharedUXPluginSetup {} @@ -28,4 +29,6 @@ export interface SharedUXPluginStart { export interface SharedUXPluginSetupDeps {} /** @internal */ -export interface SharedUXPluginStartDeps {} +export interface SharedUXPluginStartDeps { + dataViewEditor: DataViewEditorStart; +} diff --git a/src/plugins/shared_ux/tsconfig.json b/src/plugins/shared_ux/tsconfig.json index 6717616c8f2f31..069fe1d069b4a3 100644 --- a/src/plugins/shared_ux/tsconfig.json +++ b/src/plugins/shared_ux/tsconfig.json @@ -17,5 +17,11 @@ { "path": "../../core/tsconfig.json" }, + { + "path": "../data_view_editor/tsconfig.json" + }, + { + "path": "../data_views/tsconfig.json" + } ] } diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.test.ts b/src/plugins/vis_types/vega/public/data_model/search_api.test.ts index 9ecc160246e280..979f7f05cdf1d8 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.test.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.test.ts @@ -22,6 +22,7 @@ const mockComputedFields = ( getComputedFields: () => ({ runtimeFields, }), + getRuntimeMappings: () => runtimeFields, }, ]); }; diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts index 11302ad65d56bb..6a7ee55b299d01 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -32,7 +32,7 @@ export const extendSearchParamsWithRuntimeFields = async ( const indexPattern = (await indexPatterns.find(indexPatternString)).find( (index) => index.title === indexPatternString ); - runtimeMappings = indexPattern?.getComputedFields().runtimeFields; + runtimeMappings = indexPattern?.getRuntimeMappings(); } return { diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts index bc0773df056ed2..af1bdcf8b8aa24 100644 --- a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts @@ -92,8 +92,105 @@ export default function ({ getService }: FtrProviderContext) { expect(field.runtimeField.type).to.be('long'); expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); expect(field.scripted).to.be(false); + + const response3 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response3.status).to.be(400); + await supertest.delete(`${config.path}/${response1.body[config.serviceKey].id}`); }); + + it('prevents field name collisions', async () => { + const title = `basic*`; + const response1 = await supertest.post(config.path).send({ + override: true, + [config.serviceKey]: { + title, + }, + }); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response3.status).to.be(400); + + const response4 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + .send({ + name: 'runtimeComposite', + runtimeField: { + type: 'composite', + script: { + source: 'emit("a","a"); emit("b","b")', + }, + fields: { + a: { + type: 'keyword', + }, + b: { + type: 'keyword', + }, + }, + }, + }); + + expect(response4.status).to.be(200); + + const response5 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + .send({ + name: 'runtimeComposite', + runtimeField: { + type: 'composite', + script: { + source: 'emit("a","a"); emit("b","b")', + }, + fields: { + a: { + type: 'keyword', + }, + b: { + type: 'keyword', + }, + }, + }, + }); + + expect(response5.status).to.be(400); + }); }); }); }); diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts index 54c982ec7f3251..22099425df3949 100644 --- a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts @@ -59,16 +59,6 @@ export default function ({ getService }: FtrProviderContext) { expect(response1.status).to.be(404); }); - it('returns error when attempting to delete a field which is not a runtime field', async () => { - const response2 = await supertest.delete( - `${config.path}/${indexPattern.id}/runtime_field/foo` - ); - - expect(response2.status).to.be(400); - expect(response2.body.statusCode).to.be(400); - expect(response2.body.message).to.be('Only runtime fields can be deleted.'); - }); - it('returns error when ID is too long', async () => { const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; const response = await supertest.delete(`${config.path}/${id}/runtime_field/foo`); diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts index b6bebb224b33f2..f7a751387902ce 100644 --- a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts @@ -68,16 +68,6 @@ export default function ({ getService }: FtrProviderContext) { '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' ); }); - - it('returns error when attempting to fetch a field which is not a runtime field', async () => { - const response2 = await supertest.get( - `${config.path}/${indexPattern.id}/runtime_field/foo` - ); - - expect(response2.status).to.be(400); - expect(response2.body.statusCode).to.be(400); - expect(response2.body.message).to.be('Only runtime fields can be retrieved.'); - }); }); }); }); diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts index 09e781d70bb8dc..87687230897dc3 100644 --- a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts @@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; const response = await supertest.post(`${config.path}/${id}/runtime_field/foo`).send({ runtimeField: { + type: 'keyword', script: { source: "doc['something_new'].value", }, @@ -34,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { const response = await supertest.post(`${config.path}/${id}/runtime_field/foo`).send({ name: 'foo', runtimeField: { + type: 'keyword', script: { source: "doc['something_new'].value", }, diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts index 83e1b4d5716ea6..49388bb55166a6 100644 --- a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts @@ -54,6 +54,7 @@ export default function ({ getService }: FtrProviderContext) { .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) .send({ runtimeField: { + type: 'keyword', script: { source: "doc['something_new'].value", }, diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index d877a62eedc823..1f73fcc34d97d9 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -249,7 +249,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'multiple-isolated', + namespaceType: 'multiple', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index cc14ce0c760680..d609938a0d50f6 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -91,7 +91,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'multiple-isolated', + namespaceType: 'multiple', hiddenType: false, }, }, @@ -132,7 +132,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'multiple-isolated', + namespaceType: 'multiple', hiddenType: false, }, relationship: 'child', diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts new file mode 100644 index 00000000000000..580847351be9c0 --- /dev/null +++ b/test/functional/apps/console/_autocomplete.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const PageObjects = getPageObjects(['common', 'console']); + + // Failing: See https://github.com/elastic/kibana/issues/126421 + describe.skip('console autocomplete feature', function describeIndexTests() { + this.tags('includeFirefox'); + before(async () => { + log.debug('navigateTo console'); + await PageObjects.common.navigateToApp('console'); + // Ensure that the text area can be interacted with + await PageObjects.console.dismissTutorial(); + }); + + it('should provide basic auto-complete functionality', async () => { + await PageObjects.console.enterRequest(); + await PageObjects.console.promptAutocomplete(); + expect(PageObjects.console.isAutocompleteVisible()).to.be.eql(true); + }); + + describe('with a missing comma in query', () => { + const LINE_NUMBER = 4; + beforeEach(async () => { + await PageObjects.console.clearTextArea(); + await PageObjects.console.enterRequest(); + }); + it('should add a comma after previous non empty line', async () => { + await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`); + await PageObjects.console.pressEnter(); + await PageObjects.console.pressEnter(); + await PageObjects.console.pressEnter(); + await PageObjects.console.promptAutocomplete(); + await PageObjects.console.pressEnter(); + + const text = await PageObjects.console.getVisibleTextAt(LINE_NUMBER); + const lastChar = text.charAt(text.length - 1); + expect(lastChar).to.be.eql(','); + }); + + it('should add a comma after the triple quoted strings', async () => { + await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"term": """some data"""`); + await PageObjects.console.pressEnter(); + await PageObjects.console.promptAutocomplete(); + await PageObjects.console.pressEnter(); + + const text = await PageObjects.console.getVisibleTextAt(LINE_NUMBER); + const lastChar = text.charAt(text.length - 1); + expect(lastChar).to.be.eql(','); + }); + }); + }); +} diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 12d3663ebecbb3..1913a38d7573e1 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -27,8 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/124104 - describe.skip('console app', function describeIndexTests() { + describe('console app', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); @@ -82,37 +81,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(initialSize.width).to.be.greaterThan(afterSize.width); }); - it('should provide basic auto-complete functionality', async () => { - // Ensure that the text area can be interacted with - await PageObjects.console.dismissTutorial(); - expect(await PageObjects.console.hasAutocompleter()).to.be(false); - await PageObjects.console.enterRequest(); - await PageObjects.console.promptAutocomplete(); - await retry.waitFor('autocomplete to be visible', () => - PageObjects.console.hasAutocompleter() - ); - }); - - it('should add comma after previous non empty line on autocomplete', async () => { - const LINE_NUMBER = 4; - - await PageObjects.console.dismissTutorial(); - await PageObjects.console.clearTextArea(); - await PageObjects.console.enterRequest(); - - await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`); - await PageObjects.console.pressEnter(); - await PageObjects.console.pressEnter(); - await PageObjects.console.pressEnter(); - await PageObjects.console.promptAutocomplete(); - await PageObjects.console.pressEnter(); - - const textOfPreviousNonEmptyLine = await PageObjects.console.getVisibleTextAt(LINE_NUMBER); - log.debug(textOfPreviousNonEmptyLine); - const lastChar = textOfPreviousNonEmptyLine.charAt(textOfPreviousNonEmptyLine.length - 1); - expect(lastChar).to.be.equal(','); - }); - describe('with a data URI in the load_from query', () => { it('loads the data from the URI', async () => { await PageObjects.common.navigateToApp('console', { diff --git a/test/functional/apps/console/index.js b/test/functional/apps/console/index.js index 55f9dffdedb063..7a1fb578b4e4a1 100644 --- a/test/functional/apps/console/index.js +++ b/test/functional/apps/console/index.js @@ -9,8 +9,7 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/123556 - describe.skip('console app', function () { + describe('console app', function () { this.tags('ciGroup1'); before(async function () { @@ -18,5 +17,6 @@ export default function ({ getService, loadTestFile }) { }); loadTestFile(require.resolve('./_console')); + loadTestFile(require.resolve('./_autocomplete')); }); } diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index a07141a073d64b..4c9f5a5210ac68 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -118,7 +118,7 @@ export default function ({ getService, getPageObjects }) { describe('index pattern deletion', function indexDelete() { before(function () { - const expectedAlertText = 'Delete data view?'; + const expectedAlertText = 'Delete data view'; return PageObjects.settings.removeIndexPattern().then(function (alertText) { expect(alertText).to.be(expectedAlertText); }); diff --git a/test/functional/fixtures/kbn_archiver/date_nested_ccs.json b/test/functional/fixtures/kbn_archiver/date_nested_ccs.json new file mode 100644 index 00000000000000..933b21d920c00b --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/date_nested_ccs.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "nested.timestamp", + "title": "remote:date-nested" + }, + "coreMigrationVersion": "8.2.0", + "id": "remote:date-nested", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzIyLDFd" +} diff --git a/test/functional/fixtures/kbn_archiver/discover_ccs.json b/test/functional/fixtures/kbn_archiver/discover_ccs.json new file mode 100644 index 00000000000000..d53aa1bc759afb --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/discover_ccs.json @@ -0,0 +1,51 @@ +{ + "attributes": { + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", + "timeFieldName": "@timestamp", + "title": "remote:logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "remote:logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "A Saved Search", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "remote:logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "version": "WzUsMl0=" +} diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 4fdc47756e7108..e6450480bbb023 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -87,14 +87,15 @@ export class ConsolePageObject extends FtrService { const textArea = await this.testSubjects.find('console-textarea'); // There should be autocomplete for this on all license levels await textArea.pressKeys([Key.CONTROL, Key.SPACE]); + await this.retry.waitFor('autocomplete to be visible', () => this.isAutocompleteVisible()); } - public async hasAutocompleter(): Promise { - try { - return Boolean(await this.find.byCssSelector('.ace_autocomplete')); - } catch (e) { - return false; - } + public async isAutocompleteVisible() { + const element = await this.find.byCssSelector('.ace_autocomplete'); + if (!element) return false; + + const attribute = await element.getAttribute('style'); + return !attribute.includes('display: none;'); } public async enterRequest(request: string = '\nGET _search') { @@ -104,8 +105,8 @@ export class ConsolePageObject extends FtrService { } public async enterText(text: string) { - const textArea = await this.getEditorTextArea(); - await textArea.pressKeys(text); + const textArea = await this.testSubjects.find('console-textarea'); + await textArea.type(text); } private async getEditorTextArea() { @@ -138,9 +139,16 @@ export class ConsolePageObject extends FtrService { } public async clearTextArea() { - const textArea = await this.getEditorTextArea(); - await this.retry.try(async () => { + await this.retry.waitForWithTimeout('text area is cleared', 20000, async () => { + const textArea = await this.testSubjects.find('console-textarea'); + await textArea.clickMouseButton(); await textArea.clearValueWithKeyboard(); + + const editor = await this.getEditor(); + const lines = await editor.findAllByClassName('ace_line_group'); + // there should be only one empty line after clearing the textarea + const text = await lines[lines.length - 1].getVisibleText(); + return lines.length === 1 && text.trim() === ''; }); } } diff --git a/test/functional_ccs/apps/discover/_data_view_ccs.ts b/test/functional_ccs/apps/discover/_data_view_ccs.ts new file mode 100644 index 00000000000000..91d9cb2faf6816 --- /dev/null +++ b/test/functional_ccs/apps/discover/_data_view_ccs.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + + const createDataView = async (dataViewName: string) => { + await PageObjects.discover.clickIndexPatternActions(); + await PageObjects.discover.clickCreateNewDataView(); + await testSubjects.setValue('createIndexPatternNameInput', dataViewName, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await testSubjects.click('saveIndexPatternButton'); + }; + + describe('discover integration with data view editor', function describeIndexTests() { + before(async function () { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); + }); + + it('use ccs to create a new data view', async function () { + const dataViewToCreate = 'remote:logstash'; + await createDataView(dataViewToCreate); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitForWithTimeout( + 'data view selector to include a newly created dataview', + 5000, + async () => { + const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView(); + // data view editor will add wildcard symbol by default + // so we need to include it in our original title when comparing + return dataViewTitle === `${dataViewToCreate}*`; + } + ); + }); + }); +} diff --git a/test/functional_ccs/apps/discover/_saved_queries_ccs.ts b/test/functional_ccs/apps/discover/_saved_queries_ccs.ts new file mode 100644 index 00000000000000..325f279ff28ab7 --- /dev/null +++ b/test/functional_ccs/apps/discover/_saved_queries_ccs.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const browser = getService('browser'); + const filterBar = getService('filterBar'); + const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); + const testSubjects = getService('testSubjects'); + const defaultSettings = { + defaultIndex: 'logstash-*', + }; + + const setUpQueriesWithFilters = async () => { + // set up a query with filters and a time filter + log.debug('set up a query with filters to save'); + const from = 'Sep 20, 2015 @ 08:00:00.000'; + const to = 'Sep 21, 2015 @ 08:00:00.000'; + await PageObjects.common.setTime({ from, to }); + await PageObjects.common.navigateToApp('discover'); + await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); + await queryBar.setQuery('response:200'); + }; + + describe('saved queries saved objects', function describeIndexTests() { + before(async function () { + log.debug('load kibana index with default index pattern'); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/discover_ccs.json' + ); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested_ccs.json' + ); + await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + + await kibanaServer.uiSettings.replace(defaultSettings); + log.debug('discover'); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover_ccs'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/date_nested_ccs' + ); + await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await PageObjects.common.unsetTime(); + }); + + describe('saved query selection', () => { + before(async () => await setUpQueriesWithFilters()); + + it(`should unselect saved query when navigating to a 'new'`, async function () { + await savedQueryManagementComponent.saveNewQuery( + 'test-unselect-saved-query', + 'mock', + true, + true + ); + + await queryBar.submitQuery(); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(await queryBar.getQueryString()).to.eql('response:200'); + + await PageObjects.discover.clickNewSearchButton(); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('remote:date-nested'); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + await PageObjects.discover.selectIndexPattern('remote:logstash-*'); + + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); + expect(await queryBar.getQueryString()).to.eql(''); + + // reset state + await savedQueryManagementComponent.deleteSavedQuery('test-unselect-saved-query'); + }); + }); + + describe('saved query management component functionality', function () { + before(async () => await setUpQueriesWithFilters()); + + it('should show the saved query management component when there are no saved queries', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); + const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); + expect(descriptionText).to.eql( + 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + ); + }); + + it('should allow a query to be saved via the saved objects management component', async () => { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); + }); + + it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + }); + + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + await retry.waitFor( + 'the right hit count', + async () => (await PageObjects.discover.getHitCount()) === '2,792' + ); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows saving the currently loaded query as a new query', async () => { + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'OkResponseCopy', + '200 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy'); + }); + + it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => { + await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy'); + await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy'); + expect(await queryBar.getQueryString()).to.eql(''); + }); + + it('does not allow saving a query with a non-unique name', async () => { + // this check allows this test to run stand alone, also should fix occacional flakiness + const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); + if (!savedQueryExists) { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + } + await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); + }); + + it('resets any changes to a loaded query on reloading the same saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.setQuery('response:503'); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows clearing the currently loaded saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + }); + + it('allows clearing if non default language was remembered in localstorage', async () => { + await queryBar.switchQueryLanguage('lucene'); + await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url + await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.expectQueryLanguageOrFail('kql'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await queryBar.expectQueryLanguageOrFail('lucene'); + }); + + it('changing language removes saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.switchQueryLanguage('lucene'); + expect(await queryBar.getQueryString()).to.eql(''); + }); + }); + }); +} diff --git a/test/functional_ccs/apps/discover/ftr_provider_context.d.ts b/test/functional_ccs/apps/discover/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..ea232d23463e65 --- /dev/null +++ b/test/functional_ccs/apps/discover/ftr_provider_context.d.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; +import { services } from '../../../functional/services'; +import { pageObjects } from '../../../functional/page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/test/functional_ccs/apps/discover/index.ts b/test/functional_ccs/apps/discover/index.ts new file mode 100644 index 00000000000000..629423b1b75aa4 --- /dev/null +++ b/test/functional_ccs/apps/discover/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const esClient = getService('es'); + + describe('discover app css', function () { + this.tags('ciGroup6'); + + before(async function () { + await esClient.cluster.putSettings({ + persistent: { + cluster: { + remote: { + remote: { + skip_unavailable: 'true', + seeds: ['localhost:9300'], + }, + }, + }, + }, + }); + return browser.setWindowSize(1300, 800); + }); + + after(function unloadMakelogs() { + // Make sure to clean up the cluster setting from the before above. + return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + loadTestFile(require.resolve('./_data_view_ccs')); + loadTestFile(require.resolve('./_saved_queries_ccs')); + }); +} diff --git a/test/functional_ccs/config.js b/test/functional_ccs/config.js new file mode 100644 index 00000000000000..4cd88757983720 --- /dev/null +++ b/test/functional_ccs/config.js @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { services } from '../functional/services'; + +export default async function ({ readConfigFile }) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('./apps/discover')], + + services, + + junit: { + reportName: 'Kibana CCS Tests', + }, + }; +} diff --git a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts index bf3c4ff18e5700..0a1c5037e7f8a1 100644 --- a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts +++ b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts @@ -53,7 +53,6 @@ async function createIndexTemplate({ await client.indices.putIndexTemplate({ name: templateName, body: template, - // @ts-expect-error doesn't exist in @elastic/elasticsearch create: true, }); } catch (err) { diff --git a/x-pack/plugins/alerting/common/rule_task_instance.ts b/x-pack/plugins/alerting/common/rule_task_instance.ts index 59ed9097f16755..32437338b9c13a 100644 --- a/x-pack/plugins/alerting/common/rule_task_instance.ts +++ b/x-pack/plugins/alerting/common/rule_task_instance.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { rawAlertInstance } from './alert_instance'; import { DateFromString } from './date_from_string'; +import { IntervalSchedule, RuleMonitoring } from './alert'; const actionSchema = t.partial({ group: t.string, @@ -44,3 +45,9 @@ export const ruleParamsSchema = t.intersection([ }), ]); export type RuleTaskParams = t.TypeOf; + +export interface RuleExecutionRunResult { + state: RuleExecutionState; + monitoring: RuleMonitoring | undefined; + schedule: IntervalSchedule | undefined; +} diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts new file mode 100644 index 00000000000000..07f2487de20fbb --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -0,0 +1,378 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isNil } from 'lodash'; +import { Alert, AlertTypeParams, RecoveredActionGroup } from '../../common'; +import { getDefaultRuleMonitoring } from './task_runner'; +import { UntypedNormalizedRuleType } from '../rule_type_registry'; +import { TaskStatus } from '../../../task_manager/server'; +import { EVENT_LOG_ACTIONS } from '../plugin'; + +interface GeneratorParams { + [key: string]: string | number | boolean | undefined | object[] | boolean[] | object; +} + +export const RULE_NAME = 'rule-name'; +export const RULE_ID = '1'; +export const RULE_TYPE_ID = 'test'; +export const DATE_1969 = '1969-12-31T00:00:00.000Z'; +export const DATE_1970 = '1970-01-01T00:00:00.000Z'; +export const DATE_1970_5_MIN = '1969-12-31T23:55:00.000Z'; +export const MOCK_DURATION = 86400000000000; + +export const SAVED_OBJECT = { + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, + }, + references: [], +}; + +export const RULE_ACTIONS = [ + { + actionTypeId: 'action', + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + actionTypeId: 'action', + group: 'recovered', + id: '2', + params: { + isResolved: true, + }, + }, +]; + +export const SAVED_OBJECT_UPDATE_PARAMS = [ + 'alert', + '1', + { + monitoring: { + execution: { + calculated_metrics: { + success_ratio: 1, + }, + history: [ + { + success: true, + timestamp: 0, + }, + ], + }, + }, + executionStatus: { + error: null, + lastDuration: 0, + lastExecutionDate: '1970-01-01T00:00:00.000Z', + status: 'ok', + }, + }, + { refresh: false, namespace: undefined }, +]; + +export const GENERIC_ERROR_MESSAGE = 'GENERIC ERROR MESSAGE'; + +export const ruleType: jest.Mocked = { + id: RULE_TYPE_ID, + name: 'My test rule', + actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + executor: jest.fn(), + producer: 'alerts', +}; + +export const mockRunNowResponse = { + id: 1, +} as jest.ResolvedValue; + +export const mockDate = new Date('2019-02-12T21:01:22.479Z'); + +export const mockedRuleTypeSavedObject: Alert = { + id: '1', + consumer: 'bar', + createdAt: mockDate, + updatedAt: mockDate, + throttle: null, + muteAll: false, + notifyWhen: 'onActiveAlert', + enabled: true, + alertTypeId: ruleType.id, + apiKey: '', + apiKeyOwner: 'elastic', + schedule: { interval: '10s' }, + name: RULE_NAME, + tags: ['rule-', '-tags'], + createdBy: 'rule-creator', + updatedBy: 'rule-updater', + mutedInstanceIds: [], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: 'action', + params: { + foo: true, + }, + }, + { + group: RecoveredActionGroup.id, + id: '2', + actionTypeId: 'action', + params: { + isResolved: true, + }, + }, + ], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + monitoring: getDefaultRuleMonitoring(), +}; + +export const mockTaskInstance = () => ({ + id: '', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + schedule: { interval: '10s' }, + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: RULE_ID, + }, + ownerId: null, +}); + +export const generateAlertSO = (id: string) => ({ + id, + rel: 'primary', + type: 'alert', + type_id: RULE_TYPE_ID, +}); + +export const generateActionSO = (id: string) => ({ + id, + namespace: undefined, + type: 'action', + type_id: 'action', +}); + +export const generateEventLog = ({ + action, + task, + duration, + start, + end, + outcome, + reason, + instanceId, + actionSubgroup, + actionGroupId, + status, + numberOfTriggeredActions, + savedObjects = [generateAlertSO('1')], +}: GeneratorParams = {}) => ({ + ...(status === 'error' && { + error: { + message: generateErrorMessage(String(reason)), + }, + }), + event: { + action, + ...(!isNil(duration) && { duration }), + ...(start && { start }), + ...(end && { end }), + ...(outcome && { outcome }), + ...(reason && { reason }), + category: ['alerts'], + kind: 'alert', + }, + kibana: { + alert: { + rule: { + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + ...(!isNil(numberOfTriggeredActions) && { + metrics: { + number_of_triggered_actions: numberOfTriggeredActions, + number_of_searches: 3, + es_search_duration_ms: 33, + total_search_duration_ms: 23423, + }, + }), + }, + }, + }, + ...((actionSubgroup || actionGroupId || instanceId || status) && { + alerting: { + ...(actionSubgroup && { action_subgroup: actionSubgroup }), + ...(actionGroupId && { action_group_id: actionGroupId }), + ...(instanceId && { instance_id: instanceId }), + ...(status && { status }), + }, + }), + saved_objects: savedObjects, + ...(task && { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + }), + }, + message: generateMessage({ action, instanceId, actionGroupId, actionSubgroup, reason, status }), + rule: { + category: 'test', + id: '1', + license: 'basic', + ...(hasRuleName({ action, status }) && { name: RULE_NAME }), + ruleset: 'alerts', + }, +}); + +const generateMessage = ({ + action, + instanceId, + actionGroupId, + actionSubgroup, + reason, + status, +}: GeneratorParams) => { + if (action === EVENT_LOG_ACTIONS.executeStart) { + return `rule execution start: "${mockTaskInstance().params.alertId}"`; + } + + if (action === EVENT_LOG_ACTIONS.newInstance) { + return `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' created new alert: '${instanceId}'`; + } + + if (action === EVENT_LOG_ACTIONS.activeInstance) { + if (actionSubgroup) { + return `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' active alert: '${instanceId}' in actionGroup(subgroup): 'default(${actionSubgroup})'`; + } + return `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' active alert: '${instanceId}' in actionGroup: '${actionGroupId}'`; + } + + if (action === EVENT_LOG_ACTIONS.recoveredInstance) { + return `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' alert '${instanceId}' has recovered`; + } + + if (action === EVENT_LOG_ACTIONS.executeAction) { + if (actionSubgroup) { + return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup(subgroup): 'default(${actionSubgroup})' action: action:${instanceId}`; + } + return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${instanceId}`; + } + + if (action === EVENT_LOG_ACTIONS.execute) { + if (status === 'error' && reason === 'execute') { + return `rule execution failure: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`; + } + if (status === 'error') { + return `${RULE_TYPE_ID}:${RULE_ID}: execution failed`; + } + if (actionGroupId === 'recovered') { + return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${instanceId}`; + } + return `rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`; + } +}; + +const generateErrorMessage = (reason: string) => { + if (reason === 'disabled') { + return 'Rule failed to execute because rule ran after it was disabled.'; + } + return GENERIC_ERROR_MESSAGE; +}; + +export const generateRunnerResult = ({ + successRatio = 1, + history = Array(false), + state = false, + interval = '10s', +}: GeneratorParams = {}) => { + return { + monitoring: { + execution: { + calculated_metrics: { + success_ratio: successRatio, + }, + // @ts-ignore + history: history.map((success) => ({ success, timestamp: 0 })), + }, + }, + schedule: { + interval, + }, + state: { + ...(state && { alertInstances: {} }), + ...(state && { alertTypeState: undefined }), + ...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z') }), + }, + }; +}; + +export const generateEnqueueFunctionInput = () => ({ + apiKey: 'MTIzOmFiYw==', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: '1', + params: { + foo: true, + }, + relatedSavedObjects: [ + { + id: '1', + namespace: undefined, + type: 'alert', + typeId: RULE_TYPE_ID, + }, + ], + source: { + source: { + id: '1', + type: 'alert', + }, + type: 'SAVED_OBJECT', + }, + spaceId: undefined, +}); + +export const generateAlertInstance = ({ id, duration, start }: GeneratorParams = { id: 1 }) => ({ + [String(id)]: { + meta: { + lastScheduledActions: { + date: new Date(DATE_1970), + group: 'default', + subgroup: undefined, + }, + }, + state: { + bar: false, + duration, + start, + }, + }, +}); +const hasRuleName = ({ action, status }: GeneratorParams) => { + return action !== 'execute-start' && status !== 'error'; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 7496cdf7fd3367..99feefb472df1c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -19,10 +19,9 @@ import { ConcreteTaskInstance, isUnrecoverableError, RunNowResult, - TaskStatus, } from '../../../task_manager/server'; import { TaskRunnerContext } from './task_runner_factory'; -import { TaskRunner, getDefaultRuleMonitoring } from './task_runner'; +import { TaskRunner } from './task_runner'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingSystemMock, @@ -38,32 +37,42 @@ import { alertsMock, rulesClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; -import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { ExecuteOptions } from '../../../actions/server/create_execute_function'; import moment from 'moment'; +import { + generateActionSO, + generateAlertSO, + generateEventLog, + mockDate, + mockedRuleTypeSavedObject, + mockRunNowResponse, + ruleType, + RULE_NAME, + SAVED_OBJECT, + generateRunnerResult, + RULE_ACTIONS, + generateEnqueueFunctionInput, + SAVED_OBJECT_UPDATE_PARAMS, + mockTaskInstance, + GENERIC_ERROR_MESSAGE, + generateAlertInstance, + MOCK_DURATION, + DATE_1969, + DATE_1970, + DATE_1970_5_MIN, +} from './fixtures'; +import { EVENT_LOG_ACTIONS } from '../plugin'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', })); + jest.mock('../lib/wrap_scoped_cluster_client', () => ({ createWrappedScopedClusterClientFactory: jest.fn(), })); -const ruleType: jest.Mocked = { - id: 'test', - name: 'My test rule', - actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - executor: jest.fn(), - producer: 'alerts', -}; - let fakeTimer: sinon.SinonFakeTimers; const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); @@ -74,23 +83,7 @@ describe('Task Runner', () => { beforeAll(() => { fakeTimer = sinon.useFakeTimers(); - mockedTaskInstance = { - id: '', - attempts: 0, - status: TaskStatus.Running, - version: '123', - runAt: new Date(), - schedule: { interval: '10s' }, - scheduledAt: new Date(), - startedAt: new Date(), - retryAt: new Date(Date.now() + 5 * 60 * 1000), - state: {}, - taskType: 'alerting:test', - params: { - alertId: '1', - }, - ownerId: null, - }; + mockedTaskInstance = mockTaskInstance(); }); afterAll(() => fakeTimer.restore()); @@ -131,56 +124,6 @@ describe('Task Runner', () => { usageCounter: mockUsageCounter, }; - const mockDate = new Date('2019-02-12T21:01:22.479Z'); - const mockedRuleTypeSavedObject: Alert = { - id: '1', - consumer: 'bar', - createdAt: mockDate, - updatedAt: mockDate, - throttle: null, - muteAll: false, - notifyWhen: 'onActiveAlert', - enabled: true, - alertTypeId: ruleType.id, - apiKey: '', - apiKeyOwner: 'elastic', - schedule: { interval: '10s' }, - name: 'rule-name', - tags: ['rule-', '-tags'], - createdBy: 'rule-creator', - updatedBy: 'rule-updater', - mutedInstanceIds: [], - params: { - bar: true, - }, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: 'action', - params: { - foo: true, - }, - }, - { - group: RecoveredActionGroup.id, - id: '2', - actionTypeId: 'action', - params: { - isResolved: true, - }, - }, - ], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - monitoring: getDefaultRuleMonitoring(), - }; - const mockRunNowResponse = { - id: 1, - } as jest.ResolvedValue; - const ephemeralTestParams: Array< [ nameExtension: string, @@ -241,65 +184,25 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 1, - }, - "history": Array [ - Object { - "success": true, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object { - "alertInstances": Object {}, - "alertTypeState": undefined, - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ state: true, history: [true] })); expect(ruleType.executor).toHaveBeenCalledTimes(1); const call = ruleType.executor.mock.calls[0][0]; - expect(call.params).toMatchInlineSnapshot(` - Object { - "bar": true, - } - `); - expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`); - expect(call.previousStartedAt).toMatchInlineSnapshot(`1969-12-31T23:55:00.000Z`); - expect(call.state).toMatchInlineSnapshot(`Object {}`); - expect(call.name).toBe('rule-name'); + expect(call.params).toEqual({ bar: true }); + expect(call.startedAt).toStrictEqual(new Date(DATE_1970)); + expect(call.previousStartedAt).toStrictEqual(new Date(DATE_1970_5_MIN)); + expect(call.state).toEqual({}); + expect(call.name).toBe(RULE_NAME); expect(call.tags).toEqual(['rule-', '-tags']); expect(call.createdBy).toBe('rule-creator'); expect(call.updatedBy).toBe('rule-updater'); expect(call.rule).not.toBe(null); - expect(call.rule.name).toBe('rule-name'); + expect(call.rule.name).toBe(RULE_NAME); expect(call.rule.tags).toEqual(['rule-', '-tags']); expect(call.rule.consumer).toBe('bar'); expect(call.rule.enabled).toBe(true); - expect(call.rule.schedule).toMatchInlineSnapshot(` - Object { - "interval": "10s", - } - `); + expect(call.rule.schedule).toEqual({ interval: '10s' }); expect(call.rule.createdBy).toBe('rule-creator'); expect(call.rule.updatedBy).toBe('rule-updater'); expect(call.rule.createdAt).toBe(mockDate); @@ -309,26 +212,7 @@ describe('Task Runner', () => { expect(call.rule.producer).toBe('alerts'); expect(call.rule.ruleTypeId).toBe('test'); expect(call.rule.ruleTypeName).toBe('My test rule'); - expect(call.rule.actions).toMatchInlineSnapshot(` - Array [ - Object { - "actionTypeId": "action", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "action", - "group": "recovered", - "id": "2", - "params": Object { - "isResolved": true, - }, - }, - ] - `); + expect(call.rule.actions).toEqual(RULE_ACTIONS); expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); @@ -344,75 +228,16 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - } - `); + expect(eventLogger.logEvent).toHaveBeenCalledWith( + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); expect( taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith( - 'alert', - '1', - { - monitoring: { - execution: { - calculated_metrics: { - success_ratio: 1, - }, - history: [ - { - success: true, - timestamp: 0, - }, - ], - }, - }, - executionStatus: { - error: null, - lastDuration: 0, - lastExecutionDate: '1970-01-01T00:00:00.000Z', - status: 'ok', - }, - }, - { refresh: false, namespace: undefined } - ); + ).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( @@ -461,262 +286,74 @@ describe('Task Runner', () => { customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); - expect((enqueueFunction as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "1", - "params": Object { - "foo": true, - }, - "relatedSavedObjects": Array [ - Object { - "id": "1", - "namespace": undefined, - "type": "alert", - "typeId": "test", - }, - ], - "source": Object { - "source": Object { - "id": "1", - "type": "alert", - }, - "type": "SAVED_OBJECT", - }, - "spaceId": undefined, - }, - ] - `); + expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(4); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - `rule test:1: 'rule-name' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + `rule test:1: '${RULE_NAME}' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` ); expect(logger.debug).nthCalledWith( 3, 'ruleExecutionStatus for test:1: {"metrics":{"numSearches":3,"esSearchDurationMs":33,"totalSearchDurationMs":23423},"numberOfTriggeredActions":1,"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' ); - // ruleExecutionStatus for test:1: {\"lastExecutionDate\":\"1970-01-01T00:00:00.000Z\",\"status\":\"error\",\"error\":{\"reason\":\"unknown\",\"message\":\"Cannot read property 'catch' of undefined\"}} const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - event: { - action: 'execute-start', - category: ['alerts'], - kind: 'alert', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - task: { - schedule_delay: 0, - scheduled: '1970-01-01T00:00:00.000Z', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: `rule execution start: "1"`, - rule: { - category: 'test', - id: '1', - license: 'basic', - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - event: { - action: 'new-instance', - category: ['alerts'], - kind: 'alert', + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ duration: 0, - start: '1970-01-01T00:00:00.000Z', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - alerting: { - action_group_id: 'default', - action_subgroup: 'subDefault', - instance_id: '1', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: "test:1: 'rule-name' created new alert: '1'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - namespace: undefined, - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - event: { - action: 'active-instance', - category: ['alerts'], + start: DATE_1970, + action: EVENT_LOG_ACTIONS.newInstance, + actionSubgroup: 'subDefault', + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ duration: 0, - kind: 'alert', - start: '1970-01-01T00:00:00.000Z', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - alerting: { - action_group_id: 'default', - action_subgroup: 'subDefault', - instance_id: '1', - }, - saved_objects: [ - { id: '1', namespace: undefined, rel: 'primary', type: 'alert', type_id: 'test' }, - ], - }, - message: - "test:1: 'rule-name' active alert: '1' in actionGroup(subgroup): 'default(subDefault)'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - namespace: undefined, - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { - event: { - action: 'execute-action', - category: ['alerts'], - kind: 'alert', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - alerting: { - instance_id: '1', - action_group_id: 'default', - action_subgroup: 'subDefault', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - { - id: '1', - namespace: undefined, - type: 'action', - type_id: 'action', - }, - ], - }, - message: - "alert: test:1: 'rule-name' instanceId: '1' scheduled actionGroup(subgroup): 'default(subDefault)' action: action:1", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - namespace: undefined, - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(5, { - event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - metrics: { - number_of_searches: 3, - number_of_triggered_actions: 1, - es_search_duration_ms: 33, - total_search_duration_ms: 23423, - }, - }, - }, - }, - task: { - schedule_delay: 0, - scheduled: '1970-01-01T00:00:00.000Z', - }, - alerting: { - status: 'active', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: "rule executed: test:1: 'rule-name'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - ruleset: 'alerts', - }, - }); + start: DATE_1970, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + actionSubgroup: 'subDefault', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.executeAction, + actionGroupId: 'default', + instanceId: '1', + actionSubgroup: 'subDefault', + savedObjects: [generateAlertSO('1'), generateActionSO('1')], + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 5, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 1, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } ); @@ -746,15 +383,7 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, muteAll: true, }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); @@ -763,11 +392,11 @@ describe('Task Runner', () => { expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - `rule test:1: 'rule-name' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + `rule test:1: '${RULE_NAME}' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` ); expect(logger.debug).nthCalledWith( 3, - `no scheduling of actions for rule test:1: 'rule-name': rule is muted.` + `no scheduling of actions for rule test:1: '${RULE_NAME}': rule is muted.` ); expect(logger.debug).nthCalledWith( 4, @@ -777,169 +406,43 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - event: { - action: 'execute-start', - category: ['alerts'], - kind: 'alert', - }, - kibana: { - task: { - schedule_delay: 0, - scheduled: '1970-01-01T00:00:00.000Z', - }, - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: `rule execution start: \"1\"`, - rule: { - category: 'test', - id: '1', - license: 'basic', - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - event: { - action: 'new-instance', - category: ['alerts'], - kind: 'alert', + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ duration: 0, - start: '1970-01-01T00:00:00.000Z', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - alerting: { - action_group_id: 'default', - instance_id: '1', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: "test:1: 'rule-name' created new alert: '1'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - namespace: undefined, - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - event: { - action: 'active-instance', - category: ['alerts'], - kind: 'alert', + start: DATE_1970, + action: EVENT_LOG_ACTIONS.newInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ duration: 0, - start: '1970-01-01T00:00:00.000Z', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - alerting: { - instance_id: '1', - action_group_id: 'default', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - namespace: undefined, - ruleset: 'alerts', - }, - }); - expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { - event: { - action: 'execute', - category: ['alerts'], - kind: 'alert', + start: DATE_1970, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, outcome: 'success', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - metrics: { - number_of_searches: 3, - number_of_triggered_actions: 0, - es_search_duration_ms: 33, - total_search_duration_ms: 23423, - }, - }, - }, - }, - alerting: { - status: 'active', - }, - task: { - schedule_delay: 0, - scheduled: '1970-01-01T00:00:00.000Z', - }, - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, - ], - }, - message: "rule executed: test:1: 'rule-name'", - rule: { - category: 'test', - id: '1', - license: 'basic', - name: 'rule-name', - ruleset: 'alerts', - }, - }); + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -976,15 +479,7 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, mutedInstanceIds: ['2'], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); @@ -993,11 +488,11 @@ describe('Task Runner', () => { expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - `rule test:1: 'rule-name' has 2 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"},{\"instanceId\":\"2\",\"actionGroup\":\"default\"}]` + `rule test:1: '${RULE_NAME}' has 2 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"},{\"instanceId\":\"2\",\"actionGroup\":\"default\"}]` ); expect(logger.debug).nthCalledWith( 3, - `skipping scheduling of actions for '2' in rule test:1: 'rule-name': rule is muted` + `skipping scheduling of actions for '2' in rule test:1: '${RULE_NAME}': rule is muted` ); expect(logger.debug).nthCalledWith( 4, @@ -1044,8 +539,8 @@ describe('Task Runner', () => { }, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', - duration: 86400000000000, + start: DATE_1969, + duration: MOCK_DURATION, }, }, }, @@ -1057,15 +552,7 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, throttle: '1d', }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); // expect(enqueueFunction).toHaveBeenCalledTimes(1); @@ -1073,7 +560,7 @@ describe('Task Runner', () => { // expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith( 3, - `skipping scheduling of actions for '2' in rule test:1: 'rule-name': rule is throttled` + `skipping scheduling of actions for '2' in rule test:1: '${RULE_NAME}': rule is throttled` ); } ); @@ -1108,22 +595,14 @@ describe('Task Runner', () => { mutedInstanceIds: ['2'], notifyWhen: 'onActionGroupChange', }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith( 3, - `skipping scheduling of actions for '2' in rule test:1: 'rule-name': rule is muted` + `skipping scheduling of actions for '2' in rule test:1: '${RULE_NAME}': rule is muted` ); } ); @@ -1153,12 +632,12 @@ describe('Task Runner', () => { alertInstances: { '1': { meta: { - lastScheduledActions: { date: '1970-01-01T00:00:00.000Z', group: 'default' }, + lastScheduledActions: { date: DATE_1970, group: 'default' }, }, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', - duration: 86400000000000, + start: DATE_1969, + duration: MOCK_DURATION, }, }, }, @@ -1170,159 +649,40 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, notifyWhen: 'onActionGroupChange', }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 86400000000000, - "kind": "alert", - "start": "1969-12-31T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + duration: MOCK_DURATION, + start: DATE_1969, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -1370,35 +730,19 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, notifyWhen: 'onActionGroupChange', }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; await taskRunner.run(); - expect(eventLogger.logEvent.mock.calls[3][0]).toEqual( - expect.objectContaining({ - kibana: expect.objectContaining({ - alert: expect.objectContaining({ - rule: expect.objectContaining({ - execution: expect.objectContaining({ - metrics: expect.objectContaining({ - number_of_searches: 3, - number_of_triggered_actions: 1, - es_search_duration_ms: 33, - total_search_duration_ms: 23423, - }), - }), - }), - }), - }), + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 1, + task: true, }) ); expect(enqueueFunction).toHaveBeenCalledTimes(1); @@ -1457,37 +801,22 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, notifyWhen: 'onActionGroupChange', }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent.mock.calls[3][0]).toEqual( - expect.objectContaining({ - kibana: expect.objectContaining({ - alert: expect.objectContaining({ - rule: expect.objectContaining({ - execution: expect.objectContaining({ - metrics: expect.objectContaining({ - number_of_searches: 3, - number_of_triggered_actions: 1, - es_search_duration_ms: 33, - total_search_duration_ms: 23423, - }), - }), - }), - }), - }), + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 1, + task: true, }) ); + expect(enqueueFunction).toHaveBeenCalledTimes(1); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } @@ -1522,15 +851,7 @@ describe('Task Runner', () => { customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); await taskRunner.run(); expect( customTaskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest @@ -1553,266 +874,58 @@ describe('Task Runner', () => { ); expect(enqueueFunction).toHaveBeenCalledTimes(1); - expect((enqueueFunction as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "1", - "params": Object { - "foo": true, - }, - "relatedSavedObjects": Array [ - Object { - "id": "1", - "namespace": undefined, - "type": "alert", - "typeId": "test", - }, - ], - "source": Object { - "source": Object { - "id": "1", - "type": "alert", - }, - "type": "SAVED_OBJECT", - }, - "spaceId": undefined, - }, - ] - `); + expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "new-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' created new alert: '1'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute-action", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - Object { - "id": "1", - "namespace": undefined, - "type": "action", - "type_id": "action", - }, - ], - }, - "message": "alert: test:1: 'rule-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 1, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.newInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.executeAction, + actionGroupId: 'default', + instanceId: '1', + savedObjects: [generateAlertSO('1'), generateActionSO('1')], + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 5, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 1, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } ); @@ -1852,7 +965,7 @@ describe('Task Runner', () => { meta: {}, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', + start: DATE_1969, duration: 80000000000, }, }, @@ -1870,45 +983,22 @@ describe('Task Runner', () => { customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - "duration": 86400000000000, - "start": "1969-12-31T00:00:00.000Z", - }, - }, - } - `); + expect(runnerResult.state.alertInstances).toEqual( + generateAlertInstance({ id: 1, duration: MOCK_DURATION, start: DATE_1969 }) + ); const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, - `rule test:1: 'rule-name' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + `rule test:1: '${RULE_NAME}' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` ); expect(logger.debug).nthCalledWith( 3, - `rule test:1: 'rule-name' has 1 recovered alerts: [\"2\"]` + `rule test:1: '${RULE_NAME}' has 1 recovered alerts: [\"2\"]` ); expect(logger.debug).nthCalledWith( 4, @@ -1918,311 +1008,66 @@ describe('Task Runner', () => { const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "duration": 64800000000000, - "end": "1970-01-01T00:00:00.000Z", - "kind": "alert", - "start": "1969-12-31T06:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '2' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 86400000000000, - "kind": "alert", - "start": "1969-12-31T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute-action", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "recovered", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - Object { - "id": "2", - "namespace": undefined, - "type": "action", - "type_id": "action", - }, - ], - }, - "message": "alert: test:1: 'rule-name' instanceId: '2' scheduled actionGroup: 'recovered' action: action:2", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute-action", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - Object { - "id": "1", - "namespace": undefined, - "type": "action", - "type_id": "action", - }, - ], - }, - "message": "alert: test:1: 'rule-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 2, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + duration: 64800000000000, + instanceId: '2', + start: '1969-12-31T06:00:00.000Z', + end: DATE_1970, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + duration: MOCK_DURATION, + start: DATE_1969, + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.executeAction, + savedObjects: [generateAlertSO('1'), generateActionSO('2')], + actionGroupId: 'recovered', + instanceId: '2', + }) + ); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 5, + generateEventLog({ + action: EVENT_LOG_ACTIONS.executeAction, + savedObjects: [generateAlertSO('1'), generateActionSO('1')], + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 6, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 2, + task: true, + }) + ); expect(enqueueFunction).toHaveBeenCalledTimes(2); - expect((enqueueFunction as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "2", - "params": Object { - "isResolved": true, - }, - "relatedSavedObjects": Array [ - Object { - "id": "1", - "namespace": undefined, - "type": "alert", - "typeId": "test", - }, - ], - "source": Object { - "source": Object { - "id": "1", - "type": "alert", - }, - "type": "SAVED_OBJECT", - }, - "spaceId": undefined, - }, - ] - `); + expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } ); @@ -2273,41 +1118,18 @@ describe('Task Runner', () => { customTaskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: alertId, - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - }, - }, - } - `); + expect(runnerResult.state.alertInstances).toEqual(generateAlertInstance()); const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledWith( - `rule test:${alertId}: 'rule-name' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + `rule test:${alertId}: '${RULE_NAME}' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` ); expect(logger.debug).nthCalledWith( 3, - `rule test:${alertId}: 'rule-name' has 1 recovered alerts: [\"2\"]` + `rule test:${alertId}: '${RULE_NAME}' has 1 recovered alerts: [\"2\"]` ); expect(logger.debug).nthCalledWith( 4, @@ -2393,64 +1215,14 @@ describe('Task Runner', () => { }, ], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - }, - }, - } - `); + expect(runnerResult.state.alertInstances).toEqual(generateAlertInstance()); const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); expect(enqueueFunction).toHaveBeenCalledTimes(2); - expect((enqueueFunction as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "2", - "params": Object { - "isResolved": true, - }, - "relatedSavedObjects": Array [ - Object { - "id": "1", - "namespace": undefined, - "type": "alert", - "typeId": "test", - }, - ], - "source": Object { - "source": Object { - "id": "1", - "type": "alert", - }, - "type": "SAVED_OBJECT", - }, - "spaceId": undefined, - }, - ] - `); + expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } ); @@ -2481,7 +1253,7 @@ describe('Task Runner', () => { meta: { lastScheduledActions: { group: 'default', date } }, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', + start: DATE_1969, duration: 80000000000, }, }, @@ -2499,222 +1271,56 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - "duration": 86400000000000, - "start": "1969-12-31T00:00:00.000Z", - }, - }, - } - `); + expect(runnerResult.state.alertInstances).toEqual( + generateAlertInstance({ id: 1, duration: MOCK_DURATION, start: DATE_1969 }) + ); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "duration": 64800000000000, - "end": "1970-01-01T00:00:00.000Z", - "kind": "alert", - "start": "1969-12-31T06:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '2' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 86400000000000, - "kind": "alert", - "start": "1969-12-31T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + actionGroupId: 'default', + duration: 64800000000000, + instanceId: '2', + start: '1969-12-31T06:00:00.000Z', + end: DATE_1970, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + duration: MOCK_DURATION, + start: DATE_1969, + instanceId: '1', + }) + ); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + }); test('validates params before executing the alert type', async () => { const taskRunner = new TaskRunner( @@ -2736,37 +1342,9 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( `Executing Rule foo:test:1 has resulted in Error: params invalid: [param1]: expected value of type [string] but got [undefined]` ); @@ -2780,15 +1358,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); await taskRunner.run(); expect(taskRunnerFactoryInitializerParams.getRulesClientWithRequest).toHaveBeenCalledWith( @@ -2816,12 +1386,8 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - }, - references: [], + ...SAVED_OBJECT, + attributes: { enabled: true }, }); await taskRunner.run(); @@ -2853,42 +1419,12 @@ describe('Task Runner', () => { ...mockedRuleTypeSavedObject, schedule: { interval: '30s' }, }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 1, - }, - "history": Array [ - Object { - "success": true, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "30s", - }, - "state": Object { - "alertInstances": Object {}, - "alertTypeState": undefined, - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, - } - `); + expect(runnerResult).toEqual( + generateRunnerResult({ state: true, interval: '30s', history: [true] }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -2903,7 +1439,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); } ); @@ -2914,141 +1450,37 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); - + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "error": Object { - "message": "OMG", - }, - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "failure", - "reason": "execute", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "error", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution failure: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'failure', + reason: 'execute', + task: true, + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching the encrypted attributes', async () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockImplementation(() => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); }); const taskRunner = new TaskRunner( @@ -3061,129 +1493,34 @@ describe('Task Runner', () => { const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "error": Object { - "message": "OMG", - }, - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "failure", - "reason": "decrypt", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "error", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "test:1: execution failed", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'failure', + task: true, + reason: 'decrypt', + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test('recovers gracefully when the Alert Task Runner throws an exception when license is higher than supported', async () => { ruleTypeRegistry.ensureRuleTypeEnabled.mockImplementation(() => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); }); const taskRunner = new TaskRunner( @@ -3193,141 +1530,38 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "error": Object { - "message": "OMG", - }, - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "failure", - "reason": "license", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "error", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "test:1: execution failed", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'failure', + task: true, + reason: 'license', + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockImplementation(() => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); }); const taskRunner = new TaskRunner( @@ -3337,141 +1571,31 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "error": Object { - "message": "OMG", - }, - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "failure", - "reason": "unknown", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "error", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "test:1: execution failed", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'failure', + task: true, + reason: 'unknown', + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { rulesClient.get.mockImplementation(() => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); }); const taskRunner = new TaskRunner( @@ -3480,141 +1604,31 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "error": Object { - "message": "OMG", - }, - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "failure", - "reason": "read", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "error", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "test:1: execution failed", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'failure', + task: true, + reason: 'read', + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test('recovers gracefully when the Runner of a legacy Alert task which has no schedule throws an exception when fetching attributes', async () => { rulesClient.get.mockImplementation(() => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); }); // legacy alerts used to run by returning a new `runAt` instead of using a schedule @@ -3627,45 +1641,17 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "5m", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0, interval: '5m' })); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); test(`doesn't change previousStartedAt when it fails to run`, async () => { const originalAlertSate = { - previousStartedAt: '1970-01-05T00:00:00.000Z', + previousStartedAt: DATE_1970, }; ruleType.executor.mockImplementation( @@ -3678,7 +1664,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); } ); @@ -3692,15 +1678,7 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); @@ -3727,19 +1705,11 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { - expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(ex.toString()).toEqual(`Error: Saved object [alert/1] not found`); expect(logger.debug).toHaveBeenCalledWith( `Executing Rule foo:test:1 has resulted in Error: Saved object [alert/1] not found` ); @@ -3764,15 +1734,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); expect(runnerResult.schedule!.interval).toEqual(mockedTaskInstance.schedule!.interval); @@ -3794,15 +1756,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); @@ -3826,19 +1780,11 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { - expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(ex.toString()).toEqual(`Error: Saved object [alert/1] not found`); expect(logger.debug).toHaveBeenCalledWith( `Executing Rule test space:test:1 has resulted in Error: Saved object [alert/1] not found` ); @@ -3884,287 +1830,70 @@ describe('Task Runner', () => { notifyWhen: 'onActionGroupChange', actions: [], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "new-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' created new alert: '1'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "new-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' created new alert: '2'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 0, - "kind": "alert", - "start": "1970-01-01T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '2' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.newInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.newInstance, + actionGroupId: 'default', + instanceId: '2', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 5, + generateEventLog({ + duration: 0, + start: DATE_1970, + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '2', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 6, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -4196,7 +1925,7 @@ describe('Task Runner', () => { meta: {}, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', + start: DATE_1969, duration: 80000000000, }, }, @@ -4218,201 +1947,51 @@ describe('Task Runner', () => { notifyWhen: 'onActionGroupChange', actions: [], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 86400000000000, - "kind": "alert", - "start": "1969-12-31T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "duration": 64800000000000, - "kind": "alert", - "start": "1969-12-31T06:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '2' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + duration: MOCK_DURATION, + start: DATE_1969, + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + duration: 64800000000000, + start: '1969-12-31T06:00:00.000Z', + instanceId: '2', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -4458,197 +2037,45 @@ describe('Task Runner', () => { notifyWhen: 'onActionGroupChange', actions: [], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '1' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "active-instance", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "action_group_id": "default", - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' active alert: '2' in actionGroup: 'default'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "active", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.activeInstance, + actionGroupId: 'default', + instanceId: '2', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'active', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -4667,7 +2094,7 @@ describe('Task Runner', () => { meta: {}, state: { bar: false, - start: '1969-12-31T00:00:00.000Z', + start: DATE_1969, duration: 80000000000, }, }, @@ -4689,201 +2116,50 @@ describe('Task Runner', () => { notifyWhen: 'onActionGroupChange', actions: [], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "duration": 86400000000000, - "end": "1970-01-01T00:00:00.000Z", - "kind": "alert", - "start": "1969-12-31T00:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '1' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "duration": 64800000000000, - "end": "1970-01-01T00:00:00.000Z", - "kind": "alert", - "start": "1969-12-31T06:00:00.000Z", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '2' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "ok", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + duration: MOCK_DURATION, + start: DATE_1969, + end: DATE_1970, + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + duration: 64800000000000, + start: '1969-12-31T06:00:00.000Z', + end: DATE_1970, + instanceId: '2', + }) + ); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'ok', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -4926,195 +2202,44 @@ describe('Task Runner', () => { notifyWhen: 'onActionGroupChange', actions: [], }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "instance_id": "1", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '1' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "recovered-instance", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "instance_id": "2", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - }, - "message": "test:1: 'rule-name' alert '2' has recovered", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - Array [ - Object { - "event": Object { - "action": "execute", - "category": Array [ - "alerts", - ], - "kind": "alert", - "outcome": "success", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "metrics": Object { - "es_search_duration_ms": 33, - "number_of_searches": 3, - "number_of_triggered_actions": 0, - "total_search_duration_ms": 23423, - }, - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "alerting": Object { - "status": "ok", - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule executed: test:1: 'rule-name'", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "name": "rule-name", - "ruleset": "alerts", - }, - }, - ], - ] - `); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + instanceId: '1', + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 3, + generateEventLog({ + action: EVENT_LOG_ACTIONS.recoveredInstance, + instanceId: '2', + }) + ); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 4, + generateEventLog({ + action: EVENT_LOG_ACTIONS.execute, + outcome: 'success', + status: 'ok', + numberOfTriggeredActions: 0, + task: true, + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -5134,65 +2259,25 @@ describe('Task Runner', () => { } ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 1, - }, - "history": Array [ - Object { - "success": true, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object { - "alertInstances": Object {}, - "alertTypeState": undefined, - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ state: true, history: [true] })); expect(ruleType.executor).toHaveBeenCalledTimes(1); const call = ruleType.executor.mock.calls[0][0]; - expect(call.params).toMatchInlineSnapshot(` - Object { - "bar": true, - } - `); - expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`); - expect(call.previousStartedAt).toMatchInlineSnapshot(`1969-12-31T23:55:00.000Z`); - expect(call.state).toMatchInlineSnapshot(`Object {}`); - expect(call.name).toBe('rule-name'); + expect(call.params).toEqual({ bar: true }); + expect(call.startedAt).toEqual(new Date(DATE_1970)); + expect(call.previousStartedAt).toEqual(new Date(DATE_1970_5_MIN)); + expect(call.state).toEqual({}); + expect(call.name).toBe(RULE_NAME); expect(call.tags).toEqual(['rule-', '-tags']); expect(call.createdBy).toBe('rule-creator'); expect(call.updatedBy).toBe('rule-updater'); expect(call.rule).not.toBe(null); - expect(call.rule.name).toBe('rule-name'); + expect(call.rule.name).toBe(RULE_NAME); expect(call.rule.tags).toEqual(['rule-', '-tags']); expect(call.rule.consumer).toBe('bar'); expect(call.rule.enabled).toBe(true); - expect(call.rule.schedule).toMatchInlineSnapshot(` - Object { - "interval": "10s", - } - `); + expect(call.rule.schedule).toEqual({ interval: '10s' }); expect(call.rule.createdBy).toBe('rule-creator'); expect(call.rule.updatedBy).toBe('rule-updater'); expect(call.rule.createdAt).toBe(mockDate); @@ -5202,26 +2287,7 @@ describe('Task Runner', () => { expect(call.rule.producer).toBe('alerts'); expect(call.rule.ruleTypeId).toBe('test'); expect(call.rule.ruleTypeName).toBe('My test rule'); - expect(call.rule.actions).toMatchInlineSnapshot(` - Array [ - Object { - "actionTypeId": "action", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "action", - "group": "recovered", - "id": "2", - "params": Object { - "isResolved": true, - }, - }, - ] - `); + expect(call.rule.actions).toEqual(RULE_ACTIONS); expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); @@ -5237,75 +2303,16 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); - expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "event": Object { - "action": "execute-start", - "category": Array [ - "alerts", - ], - "kind": "alert", - }, - "kibana": Object { - "alert": Object { - "rule": Object { - "execution": Object { - "uuid": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - }, - }, - }, - "saved_objects": Array [ - Object { - "id": "1", - "namespace": undefined, - "rel": "primary", - "type": "alert", - "type_id": "test", - }, - ], - "task": Object { - "schedule_delay": 0, - "scheduled": "1970-01-01T00:00:00.000Z", - }, - }, - "message": "rule execution start: \\"1\\"", - "rule": Object { - "category": "test", - "id": "1", - "license": "basic", - "ruleset": "alerts", - }, - } - `); - + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); expect( taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update - ).toHaveBeenCalledWith( - 'alert', - '1', - { - monitoring: { - execution: { - calculated_metrics: { - success_ratio: 1, - }, - history: [ - { - success: true, - timestamp: 0, - }, - ], - }, - }, - executionStatus: { - error: null, - lastDuration: 0, - lastExecutionDate: '1970-01-01T00:00:00.000Z', - status: 'ok', - }, - }, - { refresh: false, namespace: undefined } - ); + ).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -5324,13 +2331,8 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: false, - }, - references: [], + ...SAVED_OBJECT, + attributes: { ...SAVED_OBJECT.attributes, enabled: false }, }); const runnerResult = await taskRunner.run(); expect(runnerResult.state.previousStartedAt?.toISOString()).toBe(state.previousStartedAt); @@ -5338,69 +2340,24 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); - expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ - event: { - action: 'execute-start', - kind: 'alert', - category: ['alerts'], - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - saved_objects: [ - { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, - ], - task: { scheduled: '1970-01-01T00:00:00.000Z', schedule_delay: 0 }, - }, - rule: { - id: '1', - license: 'basic', - category: 'test', - ruleset: 'alerts', - }, - message: 'rule execution start: "1"', - }); - expect(eventLogger.logEvent.mock.calls[1][0]).toStrictEqual({ - event: { - action: 'execute', - kind: 'alert', - category: ['alerts'], - reason: 'disabled', + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 1, + generateEventLog({ + task: true, + action: EVENT_LOG_ACTIONS.executeStart, + }) + ); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith( + 2, + generateEventLog({ + errorMessage: 'Rule failed to execute because rule ran after it was disabled.', + action: EVENT_LOG_ACTIONS.execute, outcome: 'failure', - }, - kibana: { - alert: { - rule: { - execution: { - uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - }, - }, - }, - saved_objects: [ - { rel: 'primary', type: 'alert', id: '1', namespace: undefined, type_id: 'test' }, - ], - task: { - scheduled: '1970-01-01T00:00:00.000Z', - schedule_delay: 0, - }, - alerting: { status: 'error' }, - }, - rule: { - id: '1', - license: 'basic', - category: 'test', - ruleset: 'alerts', - }, - error: { - message: 'Rule failed to execute because rule ran after it was disabled.', - }, - message: 'test:1: execution failed', - }); + task: true, + reason: 'disabled', + status: 'error', + }) + ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -5412,41 +2369,9 @@ describe('Task Runner', () => { ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 1, - }, - "history": Array [ - Object { - "success": true, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object { - "alertInstances": Object {}, - "alertTypeState": undefined, - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ state: true, history: [true] })); }); test('successfully stores failure runs', async () => { @@ -5456,15 +2381,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); ruleType.executor.mockImplementation( async ({ services: executorServices, @@ -5475,31 +2392,11 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); } ); const runnerResult = await taskRunner.run(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0, - }, - "history": Array [ - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0, success: false })); }); test('successfully stores the success ratio', async () => { @@ -5509,15 +2406,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); await taskRunner.run(); await taskRunner.run(); @@ -5532,44 +2421,14 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - throw new Error('OMG'); + throw new Error(GENERIC_ERROR_MESSAGE); } ); const runnerResult = await taskRunner.run(); ruleType.executor.mockClear(); - expect(runnerResult).toMatchInlineSnapshot(` - Object { - "monitoring": Object { - "execution": Object { - "calculated_metrics": Object { - "success_ratio": 0.75, - }, - "history": Array [ - Object { - "success": true, - "timestamp": 0, - }, - Object { - "success": true, - "timestamp": 0, - }, - Object { - "success": true, - "timestamp": 0, - }, - Object { - "success": false, - "timestamp": 0, - }, - ], - }, - }, - "schedule": Object { - "interval": "10s", - }, - "state": Object {}, - } - `); + expect(runnerResult).toEqual( + generateRunnerResult({ successRatio: 0.75, history: [true, true, true, false] }) + ); }); test('caps monitoring history at 200', async () => { @@ -5579,15 +2438,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); rulesClient.get.mockResolvedValue(mockedRuleTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - enabled: true, - }, - references: [], - }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); for (let i = 0; i < 300; i++) { await taskRunner.run(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index c5651dcf4f57b0..dbc7749a0fbdf6 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ import apm from 'elastic-apm-node'; -import { Dictionary, pickBy, mapValues, without, cloneDeep, concat, set, omit } from 'lodash'; +import { pickBy, mapValues, without, cloneDeep, concat, set, omit } from 'lodash'; import type { Request } from '@hapi/hapi'; import { UsageCounter } from 'src/plugins/usage_collection/server'; import uuid from 'uuid'; @@ -38,16 +38,16 @@ import { RawRuleExecutionStatus, AlertAction, RuleExecutionState, + RuleExecutionRunResult, } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; import { getExecutionSuccessRatio, getExecutionDurationPercentiles } from '../lib/monitoring'; import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; +import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError, isEsUnavailableError } from '../lib/is_alerting_error'; import { partiallyUpdateAlert } from '../saved_objects'; import { - ActionGroup, AlertTypeParams, AlertTypeState, AlertInstanceState, @@ -65,6 +65,14 @@ import { import { createAbortableEsClientFactory } from '../lib/create_abortable_es_client_factory'; import { createWrappedScopedClusterClientFactory } from '../lib'; import { getRecoveredAlerts } from '../lib'; +import { + GenerateNewAndRecoveredAlertEventsParams, + LogActiveAndRecoveredAlertsParams, + RuleTaskInstance, + RuleTaskRunResult, + ScheduleActionsForRecoveredAlertsParams, + TrackAlertDurationsParams, +} from './types'; const FALLBACK_RETRY_INTERVAL = '5m'; const CONNECTIVITY_RETRY_INTERVAL = '5m'; @@ -81,22 +89,6 @@ export const getDefaultRuleMonitoring = (): RuleMonitoring => ({ }, }); -interface RuleExecutionRunResult { - state: RuleExecutionState; - monitoring: RuleMonitoring | undefined; - schedule: IntervalSchedule | undefined; -} - -interface RuleTaskRunResult { - state: RuleTaskState; - monitoring: RuleMonitoring | undefined; - schedule: IntervalSchedule | undefined; -} - -interface RuleTaskInstance extends ConcreteTaskInstance { - state: RuleTaskState; -} - export class TaskRunner< Params extends AlertTypeParams, ExtractedParams extends AlertTypeParams, @@ -940,15 +932,6 @@ export class TaskRunner< } } -interface TrackAlertDurationsParams< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext -> { - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; -} - function trackAlertDurations< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext @@ -995,34 +978,6 @@ function trackAlertDurations< } } -interface GenerateNewAndRecoveredAlertEventsParams< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext -> { - eventLogger: IEventLogger; - executionId: string; - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; - ruleId: string; - ruleLabel: string; - namespace: string | undefined; - ruleType: NormalizedRuleType< - AlertTypeParams, - AlertTypeParams, - AlertTypeState, - { - [x: string]: unknown; - }, - { - [x: string]: unknown; - }, - string, - string - >; - rule: SanitizedAlert; -} - function generateNewAndRecoveredAlertEvents< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext @@ -1144,19 +1099,6 @@ function generateNewAndRecoveredAlertEvents< } } -interface ScheduleActionsForRecoveredAlertsParams< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext, - RecoveryActionGroupId extends string -> { - logger: Logger; - recoveryActionGroup: ActionGroup; - recoveredAlerts: Dictionary>; - executionHandler: ExecutionHandler; - mutedAlertIdsSet: Set; - ruleLabel: string; -} - async function scheduleActionsForRecoveredAlerts< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext, @@ -1200,19 +1142,6 @@ async function scheduleActionsForRecoveredAlerts< return triggeredActions; } -interface LogActiveAndRecoveredAlertsParams< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext, - ActionGroupIds extends string, - RecoveryActionGroupId extends string -> { - logger: Logger; - activeAlerts: Dictionary>; - recoveredAlerts: Dictionary>; - ruleLabel: string; - canSetRecoveryContext: boolean; -} - function logActiveAndRecoveredAlerts< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext, diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts new file mode 100644 index 00000000000000..c14ccfbef32202 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/types.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 { Dictionary } from 'lodash'; +import { Logger } from 'kibana/server'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, + AlertTypeState, + IntervalSchedule, + RuleExecutionState, + RuleMonitoring, + RuleTaskState, + SanitizedAlert, +} from '../../common'; +import { ConcreteTaskInstance } from '../../../task_manager/server'; +import { Alert as CreatedAlert } from '../alert'; +import { IEventLogger } from '../../../event_log/server'; +import { NormalizedRuleType } from '../rule_type_registry'; +import { ExecutionHandler } from './create_execution_handler'; + +export interface RuleTaskRunResultWithActions { + state: RuleExecutionState; + monitoring: RuleMonitoring | undefined; + schedule: IntervalSchedule | undefined; +} + +export interface RuleTaskRunResult { + state: RuleTaskState; + monitoring: RuleMonitoring | undefined; + schedule: IntervalSchedule | undefined; +} + +export interface RuleTaskInstance extends ConcreteTaskInstance { + state: RuleTaskState; +} + +export interface TrackAlertDurationsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; +} + +export interface GenerateNewAndRecoveredAlertEventsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { + eventLogger: IEventLogger; + executionId: string; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; + ruleId: string; + ruleLabel: string; + namespace: string | undefined; + ruleType: NormalizedRuleType< + AlertTypeParams, + AlertTypeParams, + AlertTypeState, + { + [x: string]: unknown; + }, + { + [x: string]: unknown; + }, + string, + string + >; + rule: SanitizedAlert; +} + +export interface ScheduleActionsForRecoveredAlertsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +> { + logger: Logger; + recoveryActionGroup: ActionGroup; + recoveredAlerts: Dictionary>; + executionHandler: ExecutionHandler; + mutedAlertIdsSet: Set; + ruleLabel: string; +} + +export interface LogActiveAndRecoveredAlertsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> { + logger: Logger; + activeAlerts: Dictionary>; + recoveredAlerts: Dictionary>; + ruleLabel: string; + canSetRecoveryContext: boolean; +} diff --git a/x-pack/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/plugins/apm/common/apm_saved_object_constants.ts index 7d9e571242afef..17c5a802a440ae 100644 --- a/x-pack/plugins/apm/common/apm_saved_object_constants.ts +++ b/x-pack/plugins/apm/common/apm_saved_object_constants.ts @@ -8,9 +8,9 @@ // the types have to match the names of the saved object mappings // in /x-pack/plugins/apm/mappings.json -// APM indices -export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; -export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices'; +// APM index settings +export const APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE = 'apm-indices'; +export const APM_INDEX_SETTINGS_SAVED_OBJECT_ID = 'apm-indices'; // APM telemetry export const APM_TELEMETRY_SAVED_OBJECT_TYPE = 'apm-telemetry'; diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 36f96f901f1ff3..84a5eeccd32281 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -55,7 +55,7 @@ export function AlertingFlyout(props: Props) { services.triggersActionsUi.getAddAlertFlyout({ consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, - alertTypeId: alertType, + ruleTypeId: alertType, canChangeTrigger: false, initialValues, metadata: { diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx deleted file mode 100644 index 1b19bb5860b2c7..00000000000000 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { ApmIndices } from '.'; -import * as hooks from '../../../../hooks/use_fetcher'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; - -describe('ApmIndices', () => { - it('should not get stuck in infinite loop', () => { - const spy = jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: undefined, - status: hooks.FETCH_STATUS.LOADING, - refetch: jest.fn(), - }); - const { getByText } = render( - - - - ); - - expect(getByText('Indices')).toMatchInlineSnapshot(` -

- Indices -

- `); - - expect(spy).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx index 1a1654e22eb199..383be90168a10b 100644 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -19,9 +20,12 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState } from 'react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; +import { ApmPluginStartDeps } from '../../../../plugin'; import { clearCache } from '../../../../services/rest/call_api'; import { APIReturnType, @@ -93,6 +97,8 @@ const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] }; export function ApmIndices() { const { core } = useApmPluginContext(); + const { services } = useKibana(); + const { notifications, application } = core; const canSave = application.capabilities.apm.save; @@ -108,6 +114,10 @@ export function ApmIndices() { [canSave] ); + const { data: space } = useFetcher(() => { + return services.spaces?.getActiveSpace(); + }, [services.spaces]); + useEffect(() => { setApmIndices( data.apmIndexSettings.reduce( @@ -191,6 +201,31 @@ export function ApmIndices() { + {space?.name && ( + <> + + + + {space?.name}, + }} + /> + + } + /> + + + + + )} + diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 952df64da840ac..75c3c290512d81 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -53,6 +53,7 @@ import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/ import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import type { SecurityPluginStart } from '../../security/public'; +import { SpacesPluginStart } from '../../spaces/public'; export type ApmPluginSetup = ReturnType; @@ -82,6 +83,7 @@ export interface ApmPluginStartDeps { observability: ObservabilityPublicStart; fleet?: FleetStart; security?: SecurityPluginStart; + spaces?: SpacesPluginStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e5ad2cb6c6c1f4..34a09fe57a05b2 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -48,6 +48,7 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; +import { migrateLegacyAPMIndicesToSpaceAware } from './saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware'; export class APMPlugin implements @@ -247,6 +248,11 @@ export class APMPlugin config: this.currentConfig, logger: this.logger, }); + + migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: this.logger, + }); } public stop() {} diff --git a/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap index 4014f2a4a2accf..00ec21b4ef3d9e 100644 --- a/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap @@ -124,7 +124,7 @@ Array [ }, "terms": Object { "field": "service.name", - "size": 50, + "size": 500, }, }, }, @@ -177,7 +177,7 @@ Array [ }, "terms": Object { "field": "service.name", - "size": 50, + "size": 500, }, }, }, diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_services_items.ts index c158f83ff55600..716fd82aefd460 100644 --- a/x-pack/plugins/apm/server/routes/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/routes/services/get_services/get_services_items.ts @@ -15,7 +15,7 @@ import { mergeServiceStats } from './merge_service_stats'; export type ServicesItemsSetup = Setup; -const MAX_NUMBER_OF_SERVICES = 50; +const MAX_NUMBER_OF_SERVICES = 500; export async function getServicesItems({ environment, diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts index 450ce3fa18dad4..7d98502ee5d939 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts @@ -7,13 +7,14 @@ import { SavedObjectsClient } from 'src/core/server'; import { - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; import { APMConfig } from '../../..'; import { APMRouteHandlerResources } from '../../typings'; import { withApmSpan } from '../../../utils/with_apm_span'; import { ApmIndicesConfig } from '../../../../../observability/common/typings'; +import { APMIndices } from '../../../saved_objects/apm_indices'; export type { ApmIndicesConfig }; @@ -22,13 +23,15 @@ type ISavedObjectsClient = Pick; async function getApmIndicesSavedObject( savedObjectsClient: ISavedObjectsClient ) { - const apmIndices = await withApmSpan('get_apm_indices_saved_object', () => - savedObjectsClient.get>( - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID - ) + const apmIndicesSavedObject = await withApmSpan( + 'get_apm_indices_saved_object', + () => + savedObjectsClient.get>( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID + ) ); - return apmIndices.attributes; + return apmIndicesSavedObject.attributes.apmIndices; } export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { @@ -90,6 +93,6 @@ export async function getApmIndexSettings({ return apmIndices.map((configurationName) => ({ configurationName, defaultValue: apmIndicesConfig[configurationName], // value defined in kibana[.dev].yml - savedValue: apmIndicesSavedObject[configurationName], // value saved via Saved Objects service + savedValue: apmIndicesSavedObject?.[configurationName], // value saved via Saved Objects service })); } diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts index bce6857aa4ff2b..0c91bccb429999 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts @@ -26,7 +26,10 @@ describe('saveApmIndices', () => { await saveApmIndices(savedObjectsClient, apmIndices); expect(savedObjectsClient.create).toHaveBeenCalledWith( expect.any(String), - { settingA: 'aa', settingF: 'ff', settingG: 'gg' }, + { + apmIndices: { settingA: 'aa', settingF: 'ff', settingG: 'gg' }, + isSpaceAware: true, + }, expect.any(Object) ); }); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts index 14a5830d8246cc..2bd4273910bbf7 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts @@ -7,9 +7,10 @@ import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; +import { APMIndices } from '../../../saved_objects/apm_indices'; import { withApmSpan } from '../../../utils/with_apm_span'; import { ApmIndicesConfig } from './get_apm_indices'; @@ -18,13 +19,10 @@ export function saveApmIndices( apmIndices: Partial ) { return withApmSpan('save_apm_indices', () => - savedObjectsClient.create( - APM_INDICES_SAVED_OBJECT_TYPE, - removeEmpty(apmIndices), - { - id: APM_INDICES_SAVED_OBJECT_ID, - overwrite: true, - } + savedObjectsClient.create( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + { apmIndices: removeEmpty(apmIndices), isSpaceAware: true }, + { id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true } ) ); } diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts index 4aa6c4953056ac..4a3b0d32e9667c 100644 --- a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts +++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts @@ -10,20 +10,43 @@ import { i18n } from '@kbn/i18n'; import { updateApmOssIndexPaths } from './migrations/update_apm_oss_index_paths'; import { ApmIndicesConfigName } from '..'; -const properties: { [Property in ApmIndicesConfigName]: { type: 'keyword' } } = - { - sourcemap: { type: 'keyword' }, - error: { type: 'keyword' }, - onboarding: { type: 'keyword' }, - span: { type: 'keyword' }, - transaction: { type: 'keyword' }, - metric: { type: 'keyword' }, +export interface APMIndices { + apmIndices?: { + sourcemap?: string; + error?: string; + onboarding?: string; + span?: string; + transaction?: string; + metric?: string; }; + isSpaceAware?: boolean; +} + +const properties: { + apmIndices: { + properties: { + [Property in ApmIndicesConfigName]: { type: 'keyword' }; + }; + }; + isSpaceAware: { type: 'boolean' }; +} = { + apmIndices: { + properties: { + sourcemap: { type: 'keyword' }, + error: { type: 'keyword' }, + onboarding: { type: 'keyword' }, + span: { type: 'keyword' }, + transaction: { type: 'keyword' }, + metric: { type: 'keyword' }, + }, + }, + isSpaceAware: { type: 'boolean' }, +}; export const apmIndices: SavedObjectsType = { name: 'apm-indices', hidden: false, - namespaceType: 'agnostic', + namespaceType: 'single', mappings: { properties }, management: { importableAndExportable: true, diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts new file mode 100644 index 00000000000000..404e08a6b112b1 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, Logger } from 'src/core/server'; +import { + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, +} from '../../../common/apm_saved_object_constants'; +import { migrateLegacyAPMIndicesToSpaceAware } from './migrate_legacy_apm_indices_to_space_aware'; + +const loggerMock = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +} as unknown as Logger; + +describe('migrateLegacyAPMIndicesToSpaceAware', () => { + describe('when legacy APM indices is not found', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + const mockFind = jest.fn(); + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: () => { + throw new Error('BOOM'); + }, + find: mockFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + + it('does not save any new saved object', () => { + migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockFind).not.toHaveBeenCalled(); + expect(mockBulkCreate).not.toHaveBeenCalled(); + expect(mockCreate).not.toHaveBeenCalled(); + }); + }); + + describe('when only default space is available', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + const mockSpaceFind = jest.fn().mockReturnValue({ + page: 1, + per_page: 10000, + total: 3, + saved_objects: [ + { + type: 'space', + id: 'default', + attributes: { + name: 'Default', + }, + references: [], + migrationVersion: { + space: '6.6.0', + }, + coreMigrationVersion: '8.2.0', + updated_at: '2022-02-22T14:13:28.839Z', + version: 'WzI4OSwxXQ==', + score: 0, + }, + ], + }); + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue({ + id: 'apm-indices', + type: 'apm-indices', + namespaces: [], + updated_at: '2022-02-22T14:17:10.584Z', + version: 'WzE1OSwxXQ==', + attributes: { + transaction: 'default-apm-*', + span: 'default-apm-*', + error: 'default-apm-*', + metric: 'default-apm-*', + sourcemap: 'default-apm-*', + onboarding: 'default-apm-*', + }, + references: [], + migrationVersion: { + 'apm-indices': '7.16.0', + }, + coreMigrationVersion: '8.2.0', + }), + find: mockSpaceFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + it('creates new default saved object with space awareness and delete legacy', async () => { + await migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockCreate).toBeCalledWith( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + { + apmIndices: { + transaction: 'default-apm-*', + span: 'default-apm-*', + error: 'default-apm-*', + metric: 'default-apm-*', + sourcemap: 'default-apm-*', + onboarding: 'default-apm-*', + }, + isSpaceAware: true, + }, + { + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + overwrite: true, + } + ); + }); + }); + + describe('when multiple spaces are found', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + + const savedObjects = [ + { id: 'default', name: 'Default' }, + { id: 'space-a', name: 'Space A' }, + { id: 'space-b', name: 'Space B' }, + ]; + const mockSpaceFind = jest.fn().mockReturnValue({ + page: 1, + per_page: 10000, + total: 3, + saved_objects: savedObjects.map(({ id, name }) => { + return { + type: 'space', + id, + attributes: { name }, + references: [], + migrationVersion: { space: '6.6.0' }, + coreMigrationVersion: '8.2.0', + updated_at: '2022-02-22T14:13:28.839Z', + version: 'WzI4OSwxXQ==', + score: 0, + }; + }), + }); + const attributes = { + transaction: 'space-apm-*', + span: 'space-apm-*', + error: 'space-apm-*', + metric: 'space-apm-*', + sourcemap: 'space-apm-*', + onboarding: 'space-apm-*', + }; + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue({ + id: 'apm-indices', + type: 'apm-indices', + namespaces: [], + updated_at: '2022-02-22T14:17:10.584Z', + version: 'WzE1OSwxXQ==', + attributes, + references: [], + migrationVersion: { + 'apm-indices': '7.16.0', + }, + coreMigrationVersion: '8.2.0', + }), + find: mockSpaceFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + it('creates multiple saved objects with space awareness and delete legacies', async () => { + await migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockCreate).toBeCalled(); + expect(mockBulkCreate).toBeCalledWith( + savedObjects + .filter(({ id }) => id !== 'default') + .map(({ id }) => { + return { + type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + initialNamespaces: [id], + attributes: { apmIndices: attributes, isSpaceAware: true }, + }; + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts new file mode 100644 index 00000000000000..130070b80ff14f --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + CoreStart, + Logger, + ISavedObjectsRepository, +} from 'src/core/server'; +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; +import { + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, +} from '../../../common/apm_saved_object_constants'; +import { ApmIndicesConfig } from '../../routes/settings/apm_indices/get_apm_indices'; +import { APMIndices } from '../apm_indices'; + +async function fetchLegacyAPMIndices(repository: ISavedObjectsRepository) { + try { + const apmIndices = await repository.get< + Partial + >(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, APM_INDEX_SETTINGS_SAVED_OBJECT_ID); + if (apmIndices.attributes.isSpaceAware) { + // This has already been migrated to become space-aware + return null; + } + return apmIndices; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // This can happen if APM is not being used + return null; + } + throw err; + } +} + +export async function migrateLegacyAPMIndicesToSpaceAware({ + coreStart, + logger, +}: { + coreStart: CoreStart; + logger: Logger; +}) { + const repository = coreStart.savedObjects.createInternalRepository(['space']); + try { + // Fetch legacy APM indices + const legacyAPMIndices = await fetchLegacyAPMIndices(repository); + + if (legacyAPMIndices === null) { + return; + } + // Fetch spaces available + const spaces = await repository.find({ + type: 'space', + page: 1, + perPage: 10_000, // max number of spaces as of 8.2 + fields: ['name'], // to avoid fetching *all* fields + }); + + const savedObjectAttributes: APMIndices = { + apmIndices: legacyAPMIndices.attributes, + isSpaceAware: true, + }; + + // Calls create first to update the default space setting isSpaceAware to true + await repository.create< + Partial + >(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, savedObjectAttributes, { + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + overwrite: true, + }); + + // Create new APM indices space aware for all spaces available + await repository.bulkCreate>( + spaces.saved_objects + // Skip default space since it was already updated + .filter(({ id: spaceId }) => spaceId !== 'default') + .map(({ id: spaceId }) => ({ + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + initialNamespaces: [spaceId], + attributes: savedObjectAttributes, + })) + ); + } catch (e) { + logger.error('Failed to migrate legacy APM indices object: ' + e.message); + } +} diff --git a/x-pack/plugins/canvas/server/lib/essql_strategy.ts b/x-pack/plugins/canvas/server/lib/essql_strategy.ts index 0cc5c8a21121b4..4e69f4831d375c 100644 --- a/x-pack/plugins/canvas/server/lib/essql_strategy.ts +++ b/x-pack/plugins/canvas/server/lib/essql_strategy.ts @@ -32,11 +32,11 @@ export const essqlSearchStrategyProvider = (): ISearchStrategy< format: 'json', body: { query, - // @ts-expect-error `params` missing from `QuerySqlRequest` type params, field_multi_value_leniency: true, time_zone: timezone, fetch_size: count, + // @ts-expect-error `client_id` missing from `QuerySqlRequest` type client_id: 'canvas', filter: { bool: { diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index bf3cc0ee320bd6..170ac2a96aaa8d 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -11,7 +11,8 @@ "kibanaVersion":"kibana", "optionalPlugins":[ "security", - "spaces" + "spaces", + "usageCollection" ], "owner":{ "githubTeam":"response-ops", diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 314796bdaa0ed9..0bd08e348c3bdc 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -21,12 +21,14 @@ import { } from '../lib/kibana/kibana_react.mock'; import { FieldHook } from '../shared_imports'; import { StartServices } from '../../types'; +import { ReleasePhase } from '../../components/types'; -interface Props { +interface TestProviderProps { children: React.ReactNode; userCanCrud?: boolean; features?: CasesFeatures; owner?: string[]; + releasePhase?: ReleasePhase; } type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -34,11 +36,12 @@ window.scrollTo = jest.fn(); const MockKibanaContextProvider = createKibanaContextProviderMock(); /** A utility for wrapping children in the providers required to run most tests */ -const TestProvidersComponent: React.FC = ({ +const TestProvidersComponent: React.FC = ({ children, features, owner = [SECURITY_SOLUTION_OWNER], userCanCrud = true, + releasePhase = 'ga', }) => { return ( @@ -63,18 +66,17 @@ export const createAppMockRenderer = ({ features, owner = [SECURITY_SOLUTION_OWNER], userCanCrud = true, -}: { - features?: CasesFeatures; - owner?: string[]; - userCanCrud?: boolean; -} = {}): AppMockRenderer => { + releasePhase = 'ga', +}: Omit = {}): AppMockRenderer => { const services = createStartServicesMock(); const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( ({ eui: euiDarkVars, darkMode: true })}> - {children} + + {children} + diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx index 954284a670fd2c..6eeff6102ae6a3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx @@ -32,6 +32,7 @@ describe('use cases add to existing case modal hook', () => { basePath: '/jest', dispatch, features: { alerts: { sync: true }, metrics: [] }, + releasePhase: 'ga', }} > {children} diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index 1f1da31595a041..70490882d31b3e 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -17,6 +17,7 @@ import { } from './cases_context_reducer'; import { CasesContextFeatures, CasesFeatures } from '../../containers/types'; import { CasesGlobalComponents } from './cases_global_components'; +import { ReleasePhase } from '../types'; export type CasesContextValueDispatch = Dispatch; @@ -27,12 +28,14 @@ export interface CasesContextValue { userCanCrud: boolean; basePath: string; features: CasesContextFeatures; + releasePhase: ReleasePhase; dispatch: CasesContextValueDispatch; } export interface CasesContextProps extends Pick { basePath?: string; features?: CasesFeatures; + releasePhase?: ReleasePhase; } export const CasesContext = React.createContext(undefined); @@ -44,7 +47,7 @@ export interface CasesContextStateValue extends Omit = ({ children, - value: { owner, userCanCrud, basePath = DEFAULT_BASE_PATH, features = {} }, + value: { owner, userCanCrud, basePath = DEFAULT_BASE_PATH, features = {}, releasePhase = 'ga' }, }) => { const { appId, appTitle } = useApplication(); const [state, dispatch] = useReducer(casesContextReducer, getInitialCasesContextState()); @@ -57,6 +60,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({ * of the DEFAULT_FEATURES object */ features: merge({}, DEFAULT_FEATURES, features), + releasePhase, dispatch, })); 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 2c3750887cb1de..103e24c4b7a656 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 @@ -29,6 +29,7 @@ describe('use cases add to new case flyout hook', () => { basePath: '/jest', dispatch, features: { alerts: { sync: true }, metrics: [] }, + releasePhase: 'ga', }} > {children} diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 86d752f84a8b38..ae50f4fd81cb69 100644 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -1,27 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EditableTitle it renders 1`] = ` - - - - - - - - +exports[`EditableTitle renders 1`] = ` + + + + + + + + + `; diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap index 17517b1d05f197..b5175e7e8c1160 100644 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap @@ -1,40 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderPage it renders 1`] = ` -
- - - + + + - - - - -

- Test supplement -

-
-
-
+ > + +

+ Test supplement +

+
+ + + +
`; diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/title.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/title.test.tsx.snap deleted file mode 100644 index 60714d6e7bb29c..00000000000000 --- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/title.test.tsx.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Title it renders 1`] = ` - -

- - - -

-
-`; - -exports[`Title it renders the title if is not a string 1`] = ` - -

- - Test title - -

-
-`; diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx index 19aea39f1f793b..9cf956c78fe726 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import '../../common/mock/match_media'; -import { TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { EditableTitle, EditableTitleProps } from './editable_title'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -27,13 +27,17 @@ describe('EditableTitle', () => { jest.clearAllMocks(); }); - test('it renders', () => { - const wrapper = shallow(); + it('renders', () => { + const wrapper = shallow( + + + + ); expect(wrapper).toMatchSnapshot(); }); - test('it does not show the edit icon when the user does not have edit permissions', () => { + it('does not show the edit icon when the user does not have edit permissions', () => { const wrapper = mount( @@ -43,7 +47,7 @@ describe('EditableTitle', () => { expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').exists()).toBeFalsy(); }); - test('it shows the edit title input field', () => { + it('shows the edit title input field', () => { const wrapper = mount( @@ -58,7 +62,7 @@ describe('EditableTitle', () => { ); }); - test('it shows the submit button', () => { + it('shows the submit button', () => { const wrapper = mount( @@ -73,7 +77,7 @@ describe('EditableTitle', () => { ); }); - test('it shows the cancel button', () => { + it('shows the cancel button', () => { const wrapper = mount( @@ -88,7 +92,7 @@ describe('EditableTitle', () => { ); }); - test('it DOES NOT shows the edit icon when in edit mode', () => { + it('DOES NOT shows the edit icon when in edit mode', () => { const wrapper = mount( @@ -103,7 +107,7 @@ describe('EditableTitle', () => { ); }); - test('it switch to non edit mode when canceled', () => { + it('switch to non edit mode when canceled', () => { const wrapper = mount( @@ -117,7 +121,7 @@ describe('EditableTitle', () => { expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true); }); - test('it should change the title', () => { + it('should change the title', () => { const newTitle = 'new test title'; const wrapper = mount( @@ -140,7 +144,7 @@ describe('EditableTitle', () => { ).toEqual(newTitle); }); - test('it should NOT change the title when cancel', () => { + it('should NOT change the title when cancel', () => { const title = 'Test title'; const newTitle = 'new test title'; @@ -164,7 +168,7 @@ describe('EditableTitle', () => { expect(wrapper.find('h1[data-test-subj="header-page-title"]').text()).toEqual(title); }); - test('it submits the title', () => { + it('submits the title', () => { const newTitle = 'new test title'; const wrapper = mount( @@ -188,7 +192,7 @@ describe('EditableTitle', () => { expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true); }); - test('it does not submits the title when the length is longer than 64 characters', () => { + it('does not submit the title when the length is longer than 64 characters', () => { const longTitle = 'This is a title that should not be saved as it is longer than 64 characters.'; @@ -216,4 +220,36 @@ describe('EditableTitle', () => { false ); }); + + describe('Badges', () => { + let appMock: AppMockRenderer; + + beforeEach(() => { + appMock = createAppMockRenderer(); + }); + + it('does not render the badge if the release is ga', () => { + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.queryByText('Beta')).toBeFalsy(); + expect(renderResult.queryByText('Technical preview')).toBeFalsy(); + }); + + it('does render the beta badge', () => { + appMock = createAppMockRenderer({ releasePhase: 'beta' }); + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.getByText('Beta')).toBeInTheDocument(); + }); + + it('does render the experimental badge', () => { + appMock = createAppMockRenderer({ releasePhase: 'experimental' }); + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.getByText('Technical preview')).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx index 674a31122d9837..95e3f6f4a4bcb3 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx @@ -22,6 +22,7 @@ import { import { MAX_TITLE_LENGTH } from '../../../common/constants'; import * as i18n from './translations'; import { Title } from './title'; +import { useCasesContext } from '../cases_context/use_cases_context'; const MyEuiButtonIcon = styled(EuiButtonIcon)` ${({ theme }) => css` @@ -48,6 +49,7 @@ const EditableTitleComponent: React.FC = ({ isLoading, title, }) => { + const { releasePhase } = useCasesContext(); const [editMode, setEditMode] = useState(false); const [errors, setErrors] = useState([]); const [newTitle, setNewTitle] = useState(title); @@ -116,22 +118,17 @@ const EditableTitleComponent: React.FC = ({
) : ( - - - - </EuiFlexItem> - <EuiFlexItem grow={false}> - {isLoading && <MySpinner data-test-subj="editable-title-loading" />} - {!isLoading && userCanCrud && ( - <MyEuiButtonIcon - aria-label={i18n.EDIT_TITLE_ARIA(title as string)} - iconType="pencil" - onClick={onClickEditIcon} - data-test-subj="editable-title-edit-icon" - /> - )} - </EuiFlexItem> - </EuiFlexGroup> + <Title title={title} releasePhase={releasePhase}> + {isLoading && <MySpinner data-test-subj="editable-title-loading" />} + {!isLoading && userCanCrud && ( + <MyEuiButtonIcon + aria-label={i18n.EDIT_TITLE_ARIA(title as string)} + iconType="pencil" + onClick={onClickEditIcon} + data-test-subj="editable-title-edit-icon" + /> + )} + ); }; EditableTitleComponent.displayName = 'EditableTitle'; diff --git a/x-pack/plugins/cases/public/components/header_page/index.test.tsx b/x-pack/plugins/cases/public/components/header_page/index.test.tsx index 4f0da554f3d1b2..bd4069cd11679a 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import '../../common/mock/match_media'; -import { TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { HeaderPage } from './index'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -21,15 +21,11 @@ describe('HeaderPage', () => { test('it renders', () => { const wrapper = shallow( - -

{'Test supplement'}

-
+ + +

{'Test supplement'}

+
+
); expect(wrapper).toMatchSnapshot(); @@ -142,4 +138,36 @@ describe('HeaderPage', () => { expect(casesHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); expect(casesHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); }); + + describe('Badges', () => { + let appMock: AppMockRenderer; + + beforeEach(() => { + appMock = createAppMockRenderer(); + }); + + it('does not render the badge if the release is ga', () => { + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.queryByText('Beta')).toBeFalsy(); + expect(renderResult.queryByText('Technical preview')).toBeFalsy(); + }); + + it('does render the beta badge', () => { + appMock = createAppMockRenderer({ releasePhase: 'beta' }); + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.getByText('Beta')).toBeInTheDocument(); + }); + + it('does render the experimental badge', () => { + appMock = createAppMockRenderer({ releasePhase: 'experimental' }); + const renderResult = appMock.render(); + + expect(renderResult.getByText('Test title')).toBeInTheDocument(); + expect(renderResult.getByText('Technical preview')).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/header_page/index.tsx b/x-pack/plugins/cases/public/components/header_page/index.tsx index 3afcd15bfa817f..db0c9bb3011c72 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.tsx @@ -6,15 +6,15 @@ */ import React, { useCallback } from 'react'; -import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import styled, { css } from 'styled-components'; -import { useAllCasesNavigation } from '../../common/navigation'; +import { useAllCasesNavigation } from '../../common/navigation'; import { LinkIcon } from '../link_icon'; import { Subtitle, SubtitleProps } from '../subtitle'; import { Title } from './title'; -import { BadgeOptions, TitleProp } from './types'; import * as i18n from './translations'; +import { useCasesContext } from '../cases_context/use_cases_context'; interface HeaderProps { border?: boolean; @@ -55,24 +55,17 @@ const LinkBack = styled.div.attrs({ `; LinkBack.displayName = 'LinkBack'; -const Badge = styled(EuiBadge)` - letter-spacing: 0; -` as unknown as typeof EuiBadge; -Badge.displayName = 'Badge'; - export interface HeaderPageProps extends HeaderProps { showBackButton?: boolean; - badgeOptions?: BadgeOptions; children?: React.ReactNode; subtitle?: SubtitleProps['items']; subtitle2?: SubtitleProps['items']; - title: TitleProp; + title: string | React.ReactNode; titleNode?: React.ReactElement; } const HeaderPageComponent: React.FC = ({ showBackButton = false, - badgeOptions, border, children, isLoading, @@ -80,8 +73,8 @@ const HeaderPageComponent: React.FC = ({ subtitle2, title, titleNode, - ...rest }) => { + const { releasePhase } = useCasesContext(); const { getAllCasesUrl, navigateToAllCases } = useAllCasesNavigation(); const navigateToAllCasesClick = useCallback( @@ -95,7 +88,7 @@ const HeaderPageComponent: React.FC = ({ ); return ( -
+
{showBackButton && ( @@ -111,7 +104,7 @@ const HeaderPageComponent: React.FC = ({ )} - {titleNode || } + {titleNode || <Title title={title} releasePhase={releasePhase} />} {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} diff --git a/x-pack/plugins/cases/public/components/header_page/title.test.tsx b/x-pack/plugins/cases/public/components/header_page/title.test.tsx index 063b21e4d89066..bd26b37956e65b 100644 --- a/x-pack/plugins/cases/public/components/header_page/title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/title.test.tsx @@ -5,41 +5,50 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render, screen } from '@testing-library/react'; import '../../common/mock/match_media'; -import { TestProviders } from '../../common/mock'; import { Title } from './title'; -import { useMountAppended } from '../../utils/use_mount_appended'; describe('Title', () => { - const mount = useMountAppended(); - - test('it renders', () => { - const wrapper = shallow( - <Title - badgeOptions={{ beta: true, text: 'Beta', tooltip: 'Test tooltip' }} - title="Test title" - /> - ); + it('does not render the badge if the release is ga', () => { + render(<Title title="Test title" releasePhase="ga" />); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.queryByText('Beta')).toBeFalsy(); + expect(screen.queryByText('Technical preview')).toBeFalsy(); }); - test('it renders the title', () => { - const wrapper = mount( - <TestProviders> - <Title title="Test title" /> - </TestProviders> - ); + it('does render the beta badge', () => { + render(<Title title="Test title" releasePhase="beta" />); + + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); + }); + + it('does render the experimental badge', () => { + render(<Title title="Test title" releasePhase="experimental" />); - expect(wrapper.find('[data-test-subj="header-page-title"]').first().exists()).toBe(true); + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.getByText('Technical preview')).toBeInTheDocument(); }); - test('it renders the title if is not a string', () => { - const wrapper = shallow(<Title title={<span>{'Test title'}</span>} />); + it('renders the title if is not a string', () => { + render(<Title title={<span>{'Test title'}</span>} releasePhase="experimental" />); + + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.getByText('Technical preview')).toBeInTheDocument(); + }); + + it('renders the children if provided', () => { + render( + <Title title="Test title" releasePhase="ga"> + <span>{'children'}</span> + + ); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.getByText('children')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/header_page/title.tsx b/x-pack/plugins/cases/public/components/header_page/title.tsx index 9ccf13b8d83a93..c6d2bf97e1cf16 100644 --- a/x-pack/plugins/cases/public/components/header_page/title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/title.tsx @@ -7,51 +7,54 @@ import React from 'react'; import { isString } from 'lodash'; -import { EuiBetaBadge, EuiBadge, EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; +import { EuiBetaBadge, EuiTitle, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { BadgeOptions, TitleProp } from './types'; import { TruncatedText } from '../truncated_text'; +import { ReleasePhase } from '../types'; +import * as i18n from './translations'; -const StyledEuiBetaBadge = styled(EuiBetaBadge)` - vertical-align: middle; -`; +interface Props { + title: string | React.ReactNode; + releasePhase: ReleasePhase; + children?: React.ReactNode; +} -StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; +const ExperimentalBadge: React.FC = () => ( + +); -const Badge = styled(EuiBadge)` - letter-spacing: 0; -` as unknown as typeof EuiBadge; -Badge.displayName = 'Badge'; +ExperimentalBadge.displayName = 'ExperimentalBadge'; -interface Props { - badgeOptions?: BadgeOptions; - title: TitleProp; -} +const BetaBadge: React.FC = () => ( + +); -const TitleComponent: React.FC = ({ title, badgeOptions }) => ( - -

- {isString(title) ? : title} - {badgeOptions && ( - <> - {' '} - {badgeOptions.beta ? ( - - ) : ( - - {badgeOptions.text} - - )} - - )} -

-
+BetaBadge.displayName = 'BetaBadge'; + +const TitleComponent: React.FC = ({ title, releasePhase, children }) => ( + + + + + +

+ {isString(title) ? : title} +

+
+
+ {children} +
+
+ + {releasePhase === 'experimental' && } + {releasePhase === 'beta' && } + +
); -TitleComponent.displayName = 'Title'; +TitleComponent.displayName = 'Title'; export const Title = React.memo(TitleComponent); diff --git a/x-pack/plugins/cases/public/components/header_page/translations.ts b/x-pack/plugins/cases/public/components/header_page/translations.ts index ba987d1f45f157..358f667bba367e 100644 --- a/x-pack/plugins/cases/public/components/header_page/translations.ts +++ b/x-pack/plugins/cases/public/components/header_page/translations.ts @@ -22,3 +22,21 @@ export const EDIT_TITLE_ARIA = (title: string) => values: { title }, defaultMessage: 'You can edit {title} by clicking', }); + +export const EXPERIMENTAL_LABEL = i18n.translate('xpack.cases.header.badge.experimentalLabel', { + defaultMessage: 'Technical preview', +}); + +export const EXPERIMENTAL_DESC = i18n.translate('xpack.cases.header.badge.experimentalDesc', { + defaultMessage: + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', +}); + +export const BETA_LABEL = i18n.translate('xpack.cases.header.badge.betaLabel', { + defaultMessage: 'Beta', +}); + +export const BETA_DESC = i18n.translate('xpack.cases.header.badge.betaDesc', { + defaultMessage: + 'This feature is currently in beta. If you encounter any bugs or have feedback, please open an issue or visit our discussion forum.', +}); diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts index 6d72a74fa5d818..d31c297d18b1c1 100644 --- a/x-pack/plugins/cases/public/components/types.ts +++ b/x-pack/plugins/cases/public/components/types.ts @@ -6,3 +6,5 @@ */ export type { CaseActionConnector } from '../../common/ui/types'; + +export type ReleasePhase = 'experimental' | 'beta' | 'ga'; diff --git a/x-pack/plugins/cases/public/methods/get_cases.tsx b/x-pack/plugins/cases/public/methods/get_cases.tsx index 94e7d321840a87..3c1d3294d38ce0 100644 --- a/x-pack/plugins/cases/public/methods/get_cases.tsx +++ b/x-pack/plugins/cases/public/methods/get_cases.tsx @@ -25,8 +25,9 @@ export const getCasesLazy = ({ refreshRef, timelineIntegration, features, + releasePhase, }: GetCasesProps) => ( - + }> { return ( }> - {children} + + {children} + ); }; diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 4cd620ac5a772f..6e9e924fbee305 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -25,7 +25,7 @@ import { getIDsAndIndicesAsArrays, } from '../../common/utils'; import { createCaseError } from '../../common/error'; -import { defaultPage, defaultPerPage } from '../../routes/api'; +import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api'; import { CasesClientArgs } from '../types'; import { combineFilters, stringToKueryNode } from '../utils'; import { Operations } from '../../authorization'; @@ -170,8 +170,8 @@ export async function find( // We need this because the default behavior of getAllCaseComments is to return all the comments // unless the page and/or perPage is specified. Since we're spreading the query after the request can // still override this behavior. - page: defaultPage, - perPage: defaultPerPage, + page: DEFAULT_PAGE, + perPage: DEFAULT_PER_PAGE, sortField: 'created_at', filter: combinedFilter, ...queryWithoutFilter, @@ -183,8 +183,8 @@ export async function find( unsecuredSavedObjectsClient, id, options: { - page: defaultPage, - perPage: defaultPerPage, + page: DEFAULT_PAGE, + perPage: DEFAULT_PER_PAGE, sortField: 'created_at', filter: combinedFilter, }, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 5881b7b7633be5..e6c4faac939389 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -8,6 +8,7 @@ import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup, @@ -15,7 +16,6 @@ import { } from '../../actions/server'; import { APP_ID } from '../common/constants'; -import { initCaseApi } from './routes/api'; import { createCaseCommentSavedObjectType, caseConfigureSavedObjectType, @@ -30,11 +30,14 @@ import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { LensServerPluginSetup } from '../../lens/server'; +import { registerRoutes } from './routes/api/register_routes'; +import { getExternalRoutes } from './routes/api/get_external_routes'; export interface PluginsSetup { - security?: SecurityPluginSetup; actions: ActionsPluginSetup; lens: LensServerPluginSetup; + usageCollection?: UsageCollectionSetup; + security?: SecurityPluginSetup; } export interface PluginsStart { @@ -100,10 +103,14 @@ export class CasePlugin { ); const router = core.http.createRouter(); - initCaseApi({ - logger: this.log, + const telemetryUsageCounter = plugins.usageCollection?.createUsageCounter(APP_ID); + + registerRoutes({ router, + routes: getExternalRoutes(), + logger: this.log, kibanaVersion: this.kibanaVersion, + telemetryUsageCounter, }); } diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts index 8a490e2f68bd0d..00a368e834a0a2 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts @@ -6,42 +6,35 @@ */ import { schema } from '@kbn/config-schema'; -import Boom from '@hapi/boom'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; import { CasesByAlertIDRequest } from '../../../../../common/api'; import { CASE_ALERTS_URL } from '../../../../../common/constants'; +import { createCaseError } from '../../../../common/error'; +import { createCasesRoute } from '../../create_cases_route'; -export function initGetCasesByAlertIdApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_ALERTS_URL, - validate: { - params: schema.object({ - alert_id: schema.string(), - }), - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const alertID = request.params.alert_id; - if (alertID == null || alertID === '') { - throw Boom.badRequest('The `alertId` is not valid'); - } - const casesClient = await context.cases.getCasesClient(); - const options = request.query as CasesByAlertIDRequest; +export const getCasesByAlertIdRoute = createCasesRoute({ + method: 'get', + path: CASE_ALERTS_URL, + params: { + params: schema.object({ + alert_id: schema.string({ minLength: 1 }), + }), + }, + handler: async ({ context, request, response }) => { + try { + const alertID = request.params.alert_id; - return response.ok({ - body: await casesClient.cases.getCasesByAlertID({ alertID, options }), - }); - } catch (error) { - logger.error( - `Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + const casesClient = await context.cases.getCasesClient(); + const options = request.query as CasesByAlertIDRequest; + + return response.ok({ + body: await casesClient.cases.getCasesByAlertID({ alertID, options }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 1784a434292cc3..a63d07037de01b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -7,32 +7,31 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; import { CASES_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initDeleteCasesApi({ router, logger }: RouteDeps) { - router.delete( - { - path: CASES_URL, - validate: { - query: schema.object({ - ids: schema.arrayOf(schema.string()), - }), - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); - await client.cases.delete(request.query.ids); +export const deleteCaseRoute = createCasesRoute({ + method: 'delete', + path: CASES_URL, + params: { + query: schema.object({ + ids: schema.arrayOf(schema.string()), + }), + }, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + await client.cases.delete(request.query.ids); - return response.noContent(); - } catch (error) { - logger.error( - `Failed to delete cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.noContent(); + } catch (error) { + throw createCaseError({ + message: `Failed to delete cases in route ids: ${JSON.stringify( + request.query.ids + )}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 8474d781a202a1..711c6909df46c0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -7,32 +7,25 @@ import { CasesFindRequest } from '../../../../common/api'; import { CASES_URL } from '../../../../common/constants'; -import { wrapError, escapeHatch } from '../utils'; -import { RouteDeps } from '../types'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initFindCasesApi({ router, logger }: RouteDeps) { - router.get( - { - path: `${CASES_URL}/_find`, - validate: { - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } - const casesClient = await context.cases.getCasesClient(); - const options = request.query as CasesFindRequest; +export const findCaseRoute = createCasesRoute({ + method: 'get', + path: `${CASES_URL}/_find`, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const options = request.query as CasesFindRequest; - return response.ok({ - body: await casesClient.cases.find({ ...options }), - }); - } catch (error) { - logger.error(`Failed to find cases in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.cases.find({ ...options }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to find cases in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index c8558d09e5c5fd..f0e53e82f14940 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -7,91 +7,83 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils'; +import { getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initGetCaseApi({ router, logger, kibanaVersion }: RouteDeps) { - router.get( - { - path: CASE_DETAILS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - query: schema.object({ - /** - * @deprecated since version 8.1.0 - */ - includeComments: schema.boolean({ defaultValue: true }), - }), - }, - }, - async (context, request, response) => { - try { - const isIncludeCommentsParamProvidedByTheUser = - request.url.searchParams.has('includeComments'); - - if (isIncludeCommentsParamProvidedByTheUser) { - logDeprecatedEndpoint( - logger, - request.headers, - `The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated` - ); - } +const params = { + params: schema.object({ + case_id: schema.string(), + }), + query: schema.object({ + /** + * @deprecated since version 8.1.0 + */ + includeComments: schema.boolean({ defaultValue: true }), + }), +}; - const casesClient = await context.cases.getCasesClient(); - const id = request.params.case_id; +export const getCaseRoute = createCasesRoute({ + method: 'get', + path: CASE_DETAILS_URL, + params, + handler: async ({ context, request, response, logger, kibanaVersion }) => { + try { + const isIncludeCommentsParamProvidedByTheUser = + request.url.searchParams.has('includeComments'); - return response.ok({ - ...(isIncludeCommentsParamProvidedByTheUser && { - headers: { - ...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'), - }, - }), - body: await casesClient.cases.get({ - id, - includeComments: request.query.includeComments, - }), - }); - } catch (error) { - logger.error( - `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}` + if (isIncludeCommentsParamProvidedByTheUser) { + logDeprecatedEndpoint( + logger, + request.headers, + `The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated` ); - return response.customError(wrapError(error)); } - } - ); - router.get( - { - path: `${CASE_DETAILS_URL}/resolve`, - validate: { - params: schema.object({ - case_id: schema.string(), + const casesClient = await context.cases.getCasesClient(); + const id = request.params.case_id; + + return response.ok({ + ...(isIncludeCommentsParamProvidedByTheUser && { + headers: { + ...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'), + }, }), - query: schema.object({ - includeComments: schema.boolean({ defaultValue: true }), + body: await casesClient.cases.get({ + id, + includeComments: request.query.includeComments, }), - }, - }, - async (context, request, response) => { - try { - const casesClient = await context.cases.getCasesClient(); - const id = request.params.case_id; + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`, + error, + }); + } + }, +}); - return response.ok({ - body: await casesClient.cases.resolve({ - id, - includeComments: request.query.includeComments, - }), - }); - } catch (error) { - logger.error( - `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}` - ); - return response.customError(wrapError(error)); - } +export const resolveCaseRoute = createCasesRoute({ + method: 'get', + path: `${CASE_DETAILS_URL}/resolve`, + params, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const id = request.params.case_id; + + return response.ok({ + body: await casesClient.cases.resolve({ + id, + includeComments: request.query.includeComments, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index 5cde28bcb01f94..c148a45220a74d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -5,35 +5,27 @@ * 2.0. */ -import { escapeHatch, wrapError } from '../utils'; -import { RouteDeps } from '../types'; import { CasesPatchRequest } from '../../../../common/api'; import { CASES_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPatchCasesApi({ router, logger }: RouteDeps) { - router.patch( - { - path: CASES_URL, - validate: { - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +export const patchCaseRoute = createCasesRoute({ + method: 'patch', + path: CASES_URL, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const cases = request.body as CasesPatchRequest; - const casesClient = await context.cases.getCasesClient(); - const cases = request.body as CasesPatchRequest; - - return response.ok({ - body: await casesClient.cases.update(cases), - }); - } catch (error) { - logger.error(`Failed to patch cases in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.cases.update(cases), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to patch cases in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index df994f18c5bbdc..226d0308a3152b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -5,35 +5,27 @@ * 2.0. */ -import { wrapError, escapeHatch } from '../utils'; - -import { RouteDeps } from '../types'; import { CasePostRequest } from '../../../../common/api'; import { CASES_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPostCaseApi({ router, logger }: RouteDeps) { - router.post( - { - path: CASES_URL, - validate: { - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } - const casesClient = await context.cases.getCasesClient(); - const theCase = request.body as CasePostRequest; +export const postCaseRoute = createCasesRoute({ + method: 'post', + path: CASES_URL, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const theCase = request.body as CasePostRequest; - return response.ok({ - body: await casesClient.cases.create({ ...theCase }), - }); - } catch (error) { - logger.error(`Failed to post case in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.cases.create({ ...theCase }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to post case in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 2b3e7954febfee..175838a9d313c0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -10,44 +10,35 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { wrapError, escapeHatch } from '../utils'; - import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { CASE_PUSH_URL } from '../../../../common/constants'; -import { RouteDeps } from '../types'; - -export function initPushCaseApi({ router, logger }: RouteDeps) { - router.post( - { - path: CASE_PUSH_URL, - validate: { - params: escapeHatch, - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +import { CaseRoute } from '../types'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; - const casesClient = await context.cases.getCasesClient(); +export const pushCaseRoute: CaseRoute = createCasesRoute({ + method: 'post', + path: CASE_PUSH_URL, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); - const params = pipe( - CasePushRequestParamsRt.decode(request.params), - fold(throwErrors(Boom.badRequest), identity) - ); + const params = pipe( + CasePushRequestParamsRt.decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); - return response.ok({ - body: await casesClient.cases.push({ - caseId: params.case_id, - connectorId: params.connector_id, - }), - }); - } catch (error) { - logger.error(`Failed to push case in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.cases.push({ + caseId: params.case_id, + connectorId: params.connector_id, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to push case in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts index 8e0d0640263ec9..ee413d73565ee1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts @@ -5,33 +5,25 @@ * 2.0. */ -import { RouteDeps } from '../../types'; -import { wrapError, escapeHatch } from '../../utils'; import { AllReportersFindRequest } from '../../../../../common/api'; import { CASE_REPORTERS_URL } from '../../../../../common/constants'; +import { createCaseError } from '../../../../common/error'; +import { createCasesRoute } from '../../create_cases_route'; -export function initGetReportersApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_REPORTERS_URL, - validate: { - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +export const getReportersRoute = createCasesRoute({ + method: 'get', + path: CASE_REPORTERS_URL, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + const options = request.query as AllReportersFindRequest; - const client = await context.cases.getCasesClient(); - const options = request.query as AllReportersFindRequest; - - return response.ok({ body: await client.cases.getReporters({ ...options }) }); - } catch (error) { - logger.error(`Failed to get reporters in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ body: await client.cases.getReporters({ ...options }) }); + } catch (error) { + throw createCaseError({ + message: `Failed to find cases in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts index 2afa96be95bc10..7dfa948aa623cd 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts @@ -5,33 +5,25 @@ * 2.0. */ -import { RouteDeps } from '../../types'; -import { wrapError, escapeHatch } from '../../utils'; import { AllTagsFindRequest } from '../../../../../common/api'; import { CASE_TAGS_URL } from '../../../../../common/constants'; +import { createCaseError } from '../../../../common/error'; +import { createCasesRoute } from '../../create_cases_route'; -export function initGetTagsApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_TAGS_URL, - validate: { - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +export const getTagsRoute = createCasesRoute({ + method: 'get', + path: CASE_TAGS_URL, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + const options = request.query as AllTagsFindRequest; - const client = await context.cases.getCasesClient(); - const options = request.query as AllTagsFindRequest; - - return response.ok({ body: await client.cases.getTags({ ...options }) }); - } catch (error) { - logger.error(`Failed to retrieve tags in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ body: await client.cases.getTags({ ...options }) }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve tags in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts index d79f90ac43935e..0a1ebd3b66a74a 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts @@ -6,35 +6,32 @@ */ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; +import { createCasesRoute } from '../create_cases_route'; +import { createCaseError } from '../../../common/error'; -export function initDeleteAllCommentsApi({ router, logger }: RouteDeps) { - router.delete( - { - path: CASE_COMMENTS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); +export const deleteAllCommentsRoute = createCasesRoute({ + method: 'delete', + path: CASE_COMMENTS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); - await client.attachments.deleteAll({ - caseID: request.params.case_id, - }); + await client.attachments.deleteAll({ + caseID: request.params.case_id, + }); - return response.noContent(); - } catch (error) { - logger.error( - `Failed to delete all comments in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.noContent(); + } catch (error) { + throw createCaseError({ + message: `Failed to delete all comments in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts index b27be46d7220d1..220fbffc76cc03 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts @@ -7,36 +7,33 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; +import { createCasesRoute } from '../create_cases_route'; +import { createCaseError } from '../../../common/error'; -export function initDeleteCommentApi({ router, logger }: RouteDeps) { - router.delete( - { - path: CASE_COMMENT_DETAILS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - comment_id: schema.string(), - }), - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); - await client.attachments.delete({ - attachmentID: request.params.comment_id, - caseID: request.params.case_id, - }); +export const deleteCommentRoute = createCasesRoute({ + method: 'delete', + path: CASE_COMMENT_DETAILS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + comment_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + await client.attachments.delete({ + attachmentID: request.params.comment_id, + caseID: request.params.case_id, + }); - return response.noContent(); - } catch (error) { - logger.error( - `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.noContent(); + } catch (error) { + throw createCaseError({ + message: `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index d4c65e6306a636..14c6090d62ea17 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts @@ -14,40 +14,36 @@ import { identity } from 'fp-ts/lib/function'; import { FindQueryParamsRt, throwErrors, excess } from '../../../../common/api'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; -import { RouteDeps } from '../types'; -import { escapeHatch, wrapError } from '../utils'; +import { createCasesRoute } from '../create_cases_route'; +import { createCaseError } from '../../../common/error'; -export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { - router.get( - { - path: `${CASE_COMMENTS_URL}/_find`, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const query = pipe( - excess(FindQueryParamsRt).decode(request.query), - fold(throwErrors(Boom.badRequest), identity) - ); +export const findCommentsRoute = createCasesRoute({ + method: 'get', + path: `${CASE_COMMENTS_URL}/_find`, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const query = pipe( + excess(FindQueryParamsRt).decode(request.query), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.attachments.find({ - caseID: request.params.case_id, - queryParams: query, - }), - }); - } catch (error) { - logger.error( - `Failed to find comments in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + const client = await context.cases.getCasesClient(); + return response.ok({ + body: await client.attachments.find({ + caseID: request.params.case_id, + queryParams: query, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to find comments in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_alerts.ts b/x-pack/plugins/cases/server/routes/api/comments/get_alerts.ts index 9c0bfac4d9c6e7..4fa793059ed630 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_alerts.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_alerts.ts @@ -7,35 +7,32 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; import { CASE_DETAILS_ALERTS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initGetAllAlertsAttachToCaseApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_DETAILS_ALERTS_URL, - validate: { - params: schema.object({ - case_id: schema.string({ minLength: 1 }), - }), - }, - }, - async (context, request, response) => { - try { - const caseId = request.params.case_id; +export const getAllAlertsAttachedToCaseRoute = createCasesRoute({ + method: 'get', + path: CASE_DETAILS_ALERTS_URL, + params: { + params: schema.object({ + case_id: schema.string({ minLength: 1 }), + }), + }, + handler: async ({ context, request, response }) => { + try { + const caseId = request.params.case_id; - const casesClient = await context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); - return response.ok({ - body: await casesClient.attachments.getAllAlertsAttachToCase({ caseId }), - }); - } catch (error) { - logger.error( - `Failed to retrieve alert ids for this case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.attachments.getAllAlertsAttachToCase({ caseId }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve alert ids for this case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index e94b19cdd9a1c1..d1e47276af1ab6 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -7,47 +7,45 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils'; +import { getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; /** * @deprecated since version 8.1.0 */ -export function initGetAllCommentsApi({ router, logger, kibanaVersion }: RouteDeps) { - router.get( - { - path: CASE_COMMENTS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - }, - }, - async (context, request, response) => { - try { - logDeprecatedEndpoint( - logger, - request.headers, - `The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.` - ); +export const getAllCommentsRoute = createCasesRoute({ + method: 'get', + path: CASE_COMMENTS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response, logger, kibanaVersion }) => { + try { + logDeprecatedEndpoint( + logger, + request.headers, + `The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.` + ); - const client = await context.cases.getCasesClient(); + const client = await context.cases.getCasesClient(); - return response.ok({ - headers: { - ...getWarningHeader(kibanaVersion), - }, - body: await client.attachments.getAll({ - caseID: request.params.case_id, - }), - }); - } catch (error) { - logger.error( - `Failed to get all comments in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, + body: await client.attachments.getAll({ + caseID: request.params.case_id, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get all comments in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts index 09805c00cb10a0..91adf832f1ea69 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts @@ -7,37 +7,34 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initGetCommentApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_COMMENT_DETAILS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - comment_id: schema.string(), - }), - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); +export const getCommentRoute = createCasesRoute({ + method: 'get', + path: CASE_COMMENT_DETAILS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + comment_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.attachments.get({ - attachmentID: request.params.comment_id, - caseID: request.params.case_id, - }), - }); - } catch (error) { - logger.error( - `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await client.attachments.get({ + attachmentID: request.params.comment_id, + caseID: request.params.case_id, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts index 5f9d885178404c..ebc17daa256110 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts @@ -11,43 +11,39 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { RouteDeps } from '../types'; -import { escapeHatch, wrapError } from '../utils'; import { CommentPatchRequestRt, throwErrors } from '../../../../common/api'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPatchCommentApi({ router, logger }: RouteDeps) { - router.patch( - { - path: CASE_COMMENTS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const query = pipe( - CommentPatchRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); +export const patchCommentRoute = createCasesRoute({ + method: 'patch', + path: CASE_COMMENTS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const query = pipe( + CommentPatchRequestRt.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = await context.cases.getCasesClient(); + const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.attachments.update({ - caseID: request.params.case_id, - updateRequest: query, - }), - }); - } catch (error) { - logger.error( - `Failed to patch comment in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await client.attachments.update({ + caseID: request.params.case_id, + updateRequest: query, + }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to patch comment in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts index ed9c9170084172..1ececb3653741d 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts @@ -6,41 +6,33 @@ */ import { schema } from '@kbn/config-schema'; -import { escapeHatch, wrapError } from '../utils'; -import { RouteDeps } from '../types'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; import { CommentRequest } from '../../../../common/api'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPostCommentApi({ router, logger }: RouteDeps) { - router.post( - { - path: CASE_COMMENTS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +export const postCommentRoute = createCasesRoute({ + method: 'post', + path: CASE_COMMENTS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const caseId = request.params.case_id; + const comment = request.body as CommentRequest; - const casesClient = await context.cases.getCasesClient(); - const caseId = request.params.case_id; - const comment = request.body as CommentRequest; - - return response.ok({ - body: await casesClient.attachments.add({ caseId, comment }), - }); - } catch (error) { - logger.error( - `Failed to post comment in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await casesClient.attachments.add({ caseId, comment }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to post comment in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts index 8222ac8fe56909..8dabf7862fc88c 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts @@ -5,31 +5,27 @@ * 2.0. */ -import { RouteDeps } from '../types'; -import { escapeHatch, wrapError } from '../utils'; import { CASE_CONFIGURE_URL } from '../../../../common/constants'; import { GetConfigureFindRequest } from '../../../../common/api'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initGetCaseConfigure({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_CONFIGURE_URL, - validate: { - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); - const options = request.query as GetConfigureFindRequest; +export const getCaseConfigureRoute = createCasesRoute({ + method: 'get', + path: CASE_CONFIGURE_URL, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + const options = request.query as GetConfigureFindRequest; - return response.ok({ - body: await client.configure.get({ ...options }), - }); - } catch (error) { - logger.error(`Failed to get case configure in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await client.configure.get({ ...options }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get case configure in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts index 46c110bbb8ba52..da99cd19065d6a 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts @@ -5,29 +5,26 @@ * 2.0. */ -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; - import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; /* * Be aware that this api will only return 20 connectors */ -export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) { - router.get( - { - path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, - validate: false, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); +export const getConnectorsRoute = createCasesRoute({ + method: 'get', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, + handler: async ({ context, response }) => { + try { + const client = await context.cases.getCasesClient(); - return response.ok({ body: await client.configure.getConnectors() }); - } catch (error) { - logger.error(`Failed to get connectors in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ body: await client.configure.getConnectors() }); + } catch (error) { + throw createCaseError({ + message: `Failed to get connectors in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts index e856a568f387a5..40b0d5123f4294 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts @@ -17,35 +17,30 @@ import { excess, } from '../../../../common/api'; import { CASE_CONFIGURE_DETAILS_URL } from '../../../../common/constants'; -import { RouteDeps } from '../types'; -import { wrapError, escapeHatch } from '../utils'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPatchCaseConfigure({ router, logger }: RouteDeps) { - router.patch( - { - path: CASE_CONFIGURE_DETAILS_URL, - validate: { - params: escapeHatch, - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const params = pipe( - excess(CaseConfigureRequestParamsRt).decode(request.params), - fold(throwErrors(Boom.badRequest), identity) - ); +export const patchCaseConfigureRoute = createCasesRoute({ + method: 'patch', + path: CASE_CONFIGURE_DETAILS_URL, + handler: async ({ context, request, response }) => { + try { + const params = pipe( + excess(CaseConfigureRequestParamsRt).decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = await context.cases.getCasesClient(); - const configuration = request.body as CasesConfigurePatch; + const client = await context.cases.getCasesClient(); + const configuration = request.body as CasesConfigurePatch; - return response.ok({ - body: await client.configure.update(params.configuration_id, configuration), - }); - } catch (error) { - logger.error(`Failed to get patch configure in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await client.configure.update(params.configuration_id, configuration), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to patch configure in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts index ed4c3529f2ca08..bb64175fb52adf 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts @@ -12,33 +12,29 @@ import { identity } from 'fp-ts/lib/function'; import { CasesConfigureRequestRt, throwErrors } from '../../../../common/api'; import { CASE_CONFIGURE_URL } from '../../../../common/constants'; -import { RouteDeps } from '../types'; -import { wrapError, escapeHatch } from '../utils'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initPostCaseConfigure({ router, logger }: RouteDeps) { - router.post( - { - path: CASE_CONFIGURE_URL, - validate: { - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - const query = pipe( - CasesConfigureRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); +export const postCaseConfigureRoute = createCasesRoute({ + method: 'post', + path: CASE_CONFIGURE_URL, + handler: async ({ context, request, response }) => { + try { + const query = pipe( + CasesConfigureRequestRt.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = await context.cases.getCasesClient(); + const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.configure.create(query), - }); - } catch (error) { - logger.error(`Failed to post case configure in route: ${error}`); - return response.customError(wrapError(error)); - } + return response.ok({ + body: await client.configure.create(query), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to post case configure in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.ts b/x-pack/plugins/cases/server/routes/api/create_cases_route.ts similarity index 61% rename from x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.ts rename to x-pack/plugins/cases/server/routes/api/create_cases_route.ts index d55921d791aded..eb6a1079440a0c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.ts +++ b/x-pack/plugins/cases/server/routes/api/create_cases_route.ts @@ -5,9 +5,6 @@ * 2.0. */ -export class PdfWorkerOutOfMemoryError extends Error { - constructor(message: string) { - super(message); - this.name = 'PdfWorkerOutOfMemoryError'; - } -} +import { CaseRoute } from './types'; + +export const createCasesRoute = (route: CaseRoute) => route; diff --git a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts new file mode 100644 index 00000000000000..7908e4eb84359f --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/get_external_routes.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 { getCasesByAlertIdRoute } from './cases/alerts/get_cases'; +import { deleteCaseRoute } from './cases/delete_cases'; +import { findCaseRoute } from './cases/find_cases'; +import { getCaseRoute, resolveCaseRoute } from './cases/get_case'; +import { patchCaseRoute } from './cases/patch_cases'; +import { postCaseRoute } from './cases/post_case'; +import { pushCaseRoute } from './cases/push_case'; +import { getReportersRoute } from './cases/reporters/get_reporters'; +import { getStatusRoute } from './stats/get_status'; +import { getUserActionsRoute } from './user_actions/get_all_user_actions'; +import { CaseRoute } from './types'; +import { getTagsRoute } from './cases/tags/get_tags'; +import { deleteAllCommentsRoute } from './comments/delete_all_comments'; +import { deleteCommentRoute } from './comments/delete_comment'; +import { findCommentsRoute } from './comments/find_comments'; +import { getCommentRoute } from './comments/get_comment'; +import { getAllCommentsRoute } from './comments/get_all_comment'; +import { patchCommentRoute } from './comments/patch_comment'; +import { postCommentRoute } from './comments/post_comment'; +import { getCaseConfigureRoute } from './configure/get_configure'; +import { getConnectorsRoute } from './configure/get_connectors'; +import { patchCaseConfigureRoute } from './configure/patch_configure'; +import { postCaseConfigureRoute } from './configure/post_configure'; +import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts'; +import { getCaseMetricRoute } from './metrics/get_case_metrics'; + +export const getExternalRoutes = () => + [ + deleteCaseRoute, + findCaseRoute, + getCaseRoute, + resolveCaseRoute, + patchCaseRoute, + postCaseRoute, + pushCaseRoute, + getUserActionsRoute, + getStatusRoute, + getCasesByAlertIdRoute, + getReportersRoute, + getTagsRoute, + deleteCommentRoute, + deleteAllCommentsRoute, + findCommentsRoute, + getCommentRoute, + getAllCommentsRoute, + patchCommentRoute, + postCommentRoute, + getCaseConfigureRoute, + getConnectorsRoute, + patchCaseConfigureRoute, + postCaseConfigureRoute, + getAllAlertsAttachedToCaseRoute, + getCaseMetricRoute, + ] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index 8298f7469f2369..31eafe0f29d28e 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -5,76 +5,11 @@ * 2.0. */ -import { initDeleteCasesApi } from './cases/delete_cases'; -import { initFindCasesApi } from '././cases/find_cases'; -import { initGetCaseApi } from './cases/get_case'; -import { initPatchCasesApi } from './cases/patch_cases'; -import { initPostCaseApi } from './cases/post_case'; -import { initPushCaseApi } from './cases/push_case'; -import { initGetReportersApi } from './cases/reporters/get_reporters'; -import { initGetCasesStatusApi } from './stats/get_status'; -import { initGetTagsApi } from './cases/tags/get_tags'; -import { initGetAllCaseUserActionsApi } from './user_actions/get_all_user_actions'; - -import { initDeleteCommentApi } from './comments/delete_comment'; -import { initDeleteAllCommentsApi } from './comments/delete_all_comments'; -import { initFindCaseCommentsApi } from './comments/find_comments'; -import { initGetAllCommentsApi } from './comments/get_all_comment'; -import { initGetCommentApi } from './comments/get_comment'; -import { initPatchCommentApi } from './comments/patch_comment'; -import { initPostCommentApi } from './comments/post_comment'; - -import { initCaseConfigureGetActionConnector } from './configure/get_connectors'; -import { initGetCaseConfigure } from './configure/get_configure'; -import { initPatchCaseConfigure } from './configure/patch_configure'; -import { initPostCaseConfigure } from './configure/post_configure'; - -import { RouteDeps } from './types'; -import { initGetCasesByAlertIdApi } from './cases/alerts/get_cases'; -import { initGetAllAlertsAttachToCaseApi } from './comments/get_alerts'; -import { initGetCaseMetricsApi } from './metrics/get_case_metrics'; - /** * Default page number when interacting with the saved objects API. */ -export const defaultPage = 1; +export const DEFAULT_PAGE = 1; /** * Default number of results when interacting with the saved objects API. */ -export const defaultPerPage = 20; - -export function initCaseApi(deps: RouteDeps) { - // Cases - initDeleteCasesApi(deps); - initFindCasesApi(deps); - initGetCaseApi(deps); - initPatchCasesApi(deps); - initPostCaseApi(deps); - initPushCaseApi(deps); - initGetAllCaseUserActionsApi(deps); - - // Comments - initDeleteCommentApi(deps); - initDeleteAllCommentsApi(deps); - initFindCaseCommentsApi(deps); - initGetCommentApi(deps); - initGetAllCommentsApi(deps); - initPatchCommentApi(deps); - initPostCommentApi(deps); - // Cases Configure - initCaseConfigureGetActionConnector(deps); - initGetCaseConfigure(deps); - initPatchCaseConfigure(deps); - initPostCaseConfigure(deps); - // Reporters - initGetReportersApi(deps); - // Status - initGetCasesStatusApi(deps); - // Tags - initGetTagsApi(deps); - // Alerts - initGetCasesByAlertIdApi(deps); - initGetAllAlertsAttachToCaseApi(deps); - // Metrics - initGetCaseMetricsApi(deps); -} +export const DEFAULT_PER_PAGE = 20; diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts index 0cfad10b28316e..b86b84410abe62 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts @@ -7,37 +7,35 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; - import { CASE_METRICS_DETAILS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; -export function initGetCaseMetricsApi({ router, logger }: RouteDeps) { - router.get( - { - path: CASE_METRICS_DETAILS_URL, - validate: { - params: schema.object({ - case_id: schema.string({ minLength: 1 }), - }), - query: schema.object({ - features: schema.arrayOf(schema.string({ minLength: 1 })), +export const getCaseMetricRoute = createCasesRoute({ + method: 'get', + path: CASE_METRICS_DETAILS_URL, + params: { + params: schema.object({ + case_id: schema.string({ minLength: 1 }), + }), + query: schema.object({ + features: schema.arrayOf(schema.string({ minLength: 1 })), + }), + }, + handler: async ({ context, request, response }) => { + try { + const client = await context.cases.getCasesClient(); + return response.ok({ + body: await client.metrics.getCaseMetrics({ + caseId: request.params.case_id, + features: request.query.features, }), - }, - }, - async (context, request, response) => { - try { - const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.metrics.getCaseMetrics({ - caseId: request.params.case_id, - features: request.query.features, - }), - }); - } catch (error) { - logger.error(`Failed to get case metrics in route: ${error}`); - return response.customError(wrapError(error)); - } + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get case metrics in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/register_routes.test.ts b/x-pack/plugins/cases/server/routes/api/register_routes.test.ts new file mode 100644 index 00000000000000..71ec6f2bce5cd6 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/register_routes.test.ts @@ -0,0 +1,280 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { + httpServerMock, + httpServiceMock, + loggingSystemMock, +} from '../../../../../../src/core/server/mocks'; + +import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/server/mocks'; + +import { CasesRouter } from '../../types'; +import { createCasesRoute } from './create_cases_route'; +import { registerRoutes } from './register_routes'; +import { CaseRoute } from './types'; + +describe('registerRoutes', () => { + let router: jest.Mocked; + const logger = loggingSystemMock.createLogger(); + const response = httpServerMock.createResponseFactory(); + const telemetryUsageCounter = usageCollectionPluginMock + .createSetupContract() + .createUsageCounter('test'); + + const handler = jest.fn(); + const customError = jest.fn(); + const badRequest = jest.fn(); + + const routes = [ + createCasesRoute({ + method: 'get', + path: '/foo/{case_id}', + params: { + params: schema.object({ + case_id: schema.string(), + }), + query: schema.object({ + includeComments: schema.boolean(), + }), + }, + handler, + }), + + createCasesRoute({ + method: 'post', + path: '/bar', + params: { + body: schema.object({ + title: schema.string(), + }), + }, + handler: async () => response.ok(), + }), + createCasesRoute({ + method: 'put', + path: '/baz', + handler: async () => response.ok(), + }), + createCasesRoute({ + method: 'patch', + path: '/qux', + handler: async () => response.ok(), + }), + createCasesRoute({ + method: 'delete', + path: '/quux', + handler: async () => response.ok(), + }), + ] as CaseRoute[]; + + const initApi = (casesRoutes: CaseRoute[]) => { + registerRoutes({ + router, + logger, + routes: casesRoutes, + kibanaVersion: '8.2.0', + telemetryUsageCounter, + }); + + const simulateRequest = async ({ + method, + path, + context = { cases: {} }, + headers = {}, + }: { + method: keyof Pick; + path: string; + context?: Record; + headers?: Record; + }) => { + const [, registeredRouteHandler] = + // @ts-ignore + router[method].mock.calls.find((call) => { + return call[0].path === path; + }) ?? []; + + const result = await registeredRouteHandler( + context, + { headers }, + { customError, badRequest } + ); + return result; + }; + + return { + simulateRequest, + }; + }; + + const initAndSimulateError = async () => { + const { simulateRequest } = initApi([ + ...routes, + createCasesRoute({ + method: 'get', + path: '/error', + handler: async () => { + throw new Error('API error'); + }, + }), + ]); + + await simulateRequest({ + method: 'get', + path: '/error', + }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + router = httpServiceMock.createRouter(); + }); + + describe('api registration', () => { + const endpoints: Array<[CaseRoute['method'], string]> = [ + ['get', '/foo/{case_id}'], + ['post', '/bar'], + ['put', '/baz'], + ['patch', '/qux'], + ['delete', '/quux'], + ]; + + it('registers the endpoints correctly', () => { + initApi(routes); + + for (const endpoint of endpoints) { + const [method, path] = endpoint; + + expect(router[method]).toHaveBeenCalledTimes(1); + expect(router[method]).toBeCalledWith( + { path, validate: expect.anything() }, + expect.anything() + ); + } + }); + }); + + describe('api validation', () => { + const validation: Array< + ['params' | 'query' | 'body', keyof CasesRouter, Record] + > = [ + ['params', 'get', { case_id: '123' }], + ['query', 'get', { includeComments: false }], + ['body', 'post', { title: 'test' }], + ]; + + describe.each(validation)('%s', (type, method, value) => { + it(`validates ${type} correctly`, () => { + initApi(routes); + // @ts-ignore + const params = router[method].mock.calls[0][0].validate[type]; + expect(() => params.validate(value)).not.toThrow(); + }); + + it(`throws if ${type} is wrong`, () => { + initApi(routes); + // @ts-ignore + const params = router[method].mock.calls[0][0].validate[type]; + expect(() => params.validate({})).toThrow(); + }); + + it(`skips path parameter validation if ${type} is not provided`, () => { + initApi(routes); + // @ts-ignore + const params = router.put.mock.calls[0][0].validate[type]; + expect(() => params.validate({})).not.toThrow(); + }); + }); + }); + + describe('handler execution', () => { + it('calls the handler correctly', async () => { + const { simulateRequest } = initApi(routes); + await simulateRequest({ method: 'get', path: '/foo/{case_id}' }); + expect(handler).toHaveBeenCalled(); + }); + }); + + describe('telemetry', () => { + it('increases the counters correctly on a successful kibana request', async () => { + const { simulateRequest } = initApi(routes); + await simulateRequest({ + method: 'get', + path: '/foo/{case_id}', + headers: { 'kbn-version': '8.2.0', referer: 'https://example.com' }, + }); + expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'GET /foo/{case_id}', + counterType: 'success', + }); + + expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'GET /foo/{case_id}', + counterType: 'kibanaRequest.yes', + }); + }); + + it('increases the counters correctly on a successful non kibana request', async () => { + const { simulateRequest } = initApi(routes); + await simulateRequest({ + method: 'get', + path: '/foo/{case_id}', + }); + expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'GET /foo/{case_id}', + counterType: 'success', + }); + + expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'GET /foo/{case_id}', + counterType: 'kibanaRequest.no', + }); + }); + + it('increases the counters correctly on an error', async () => { + await initAndSimulateError(); + + expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'GET /error', + counterType: 'error', + }); + }); + }); + + describe('errors', () => { + it('logs the error', async () => { + await initAndSimulateError(); + + expect(logger.error).toBeCalledWith('API error'); + }); + + it('returns an error response', async () => { + await initAndSimulateError(); + + expect(customError).toBeCalledWith({ + body: expect.anything(), + headers: {}, + statusCode: 500, + }); + }); + + it('returns an error response when the case context is not registered', async () => { + const { simulateRequest } = initApi(routes); + await simulateRequest({ + method: 'get', + path: '/foo/{case_id}', + context: {}, + }); + + expect(badRequest).toBeCalledWith({ + body: 'RouteHandlerContext is not registered for cases', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/routes/api/register_routes.ts b/x-pack/plugins/cases/server/routes/api/register_routes.ts new file mode 100644 index 00000000000000..843009f3b22c5a --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/register_routes.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteRegistrar } from 'kibana/server'; +import { CasesRequestHandlerContext } from '../../types'; +import { CaseRoute, RegisterRoutesDeps } from './types'; +import { escapeHatch, getIsKibanaRequest, wrapError } from './utils'; + +const increaseTelemetryCounters = ({ + telemetryUsageCounter, + method, + path, + isKibanaRequest, + isError = false, +}: { + telemetryUsageCounter: Exclude; + method: string; + path: string; + isKibanaRequest: boolean; + isError?: boolean; +}) => { + const counterName = `${method.toUpperCase()} ${path}`; + + telemetryUsageCounter.incrementCounter({ + counterName, + counterType: isError ? 'error' : 'success', + }); + + telemetryUsageCounter.incrementCounter({ + counterName, + counterType: `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + }); +}; + +export const registerRoutes = (deps: RegisterRoutesDeps) => { + const { router, routes, logger, kibanaVersion, telemetryUsageCounter } = deps; + + routes.forEach((route) => { + const { method, path, params, handler } = route as CaseRoute; + + (router[method] as RouteRegistrar)( + { + path, + validate: { + params: params?.params ?? escapeHatch, + query: params?.query ?? escapeHatch, + body: params?.body ?? schema.nullable(escapeHatch), + }, + }, + async (context, request, response) => { + const isKibanaRequest = getIsKibanaRequest(request.headers); + + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + try { + const res = await handler({ logger, context, request, response, kibanaVersion }); + + if (telemetryUsageCounter) { + increaseTelemetryCounters({ telemetryUsageCounter, method, path, isKibanaRequest }); + } + + return res; + } catch (error) { + logger.error(error.message); + + if (telemetryUsageCounter) { + increaseTelemetryCounters({ + telemetryUsageCounter, + method, + path, + isError: true, + isKibanaRequest, + }); + } + + return response.customError(wrapError(error)); + } + } + ); + }); +}; diff --git a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts index 90044c9516b3a8..4cd5bd7eebd0a6 100644 --- a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -5,40 +5,40 @@ * 2.0. */ -import { RouteDeps } from '../types'; -import { escapeHatch, wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils'; +import { CaseRoute } from '../types'; +import { getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CasesStatusRequest } from '../../../../common/api'; import { CASE_STATUS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; /** * @deprecated since version 8.1.0 */ -export function initGetCasesStatusApi({ router, logger, kibanaVersion }: RouteDeps) { - router.get( - { - path: CASE_STATUS_URL, - validate: { query: escapeHatch }, - }, - async (context, request, response) => { - try { - logDeprecatedEndpoint( - logger, - request.headers, - `The get cases status API '${CASE_STATUS_URL}' is deprecated.` - ); +export const getStatusRoute: CaseRoute = createCasesRoute({ + method: 'get', + path: CASE_STATUS_URL, + handler: async ({ context, request, response, logger, kibanaVersion }) => { + try { + logDeprecatedEndpoint( + logger, + request.headers, + `The get cases status API '${CASE_STATUS_URL}' is deprecated.` + ); - const client = await context.cases.getCasesClient(); - return response.ok({ - headers: { - ...getWarningHeader(kibanaVersion), - }, - body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest), - }); - } catch (error) { - logger.error(`Failed to get status stats in route: ${error}`); - return response.customError(wrapError(error)); - } + const client = await context.cases.getCasesClient(); + return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, + body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get status stats in route: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index e3aa6e0e970fa2..2b1893ebb75c7e 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -5,17 +5,44 @@ * 2.0. */ -import type { Logger, PluginInitializerContext } from 'kibana/server'; +import type { + Logger, + PluginInitializerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, + RouteValidatorConfig, +} from 'kibana/server'; -import type { CasesRouter } from '../../types'; +import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/server'; +import type { CasesRequestHandlerContext, CasesRouter } from '../../types'; -export interface RouteDeps { +type TelemetryUsageCounter = ReturnType; + +export interface RegisterRoutesDeps { router: CasesRouter; + routes: CaseRoute[]; logger: Logger; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + telemetryUsageCounter?: TelemetryUsageCounter; } export interface TotalCommentByCase { caseId: string; totalComments: number; } + +interface CaseRouteHandlerArguments { + request: KibanaRequest; + context: CasesRequestHandlerContext; + response: KibanaResponseFactory; + logger: Logger; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; +} + +export interface CaseRoute

{ + method: 'get' | 'post' | 'put' | 'delete' | 'patch'; + path: string; + params?: RouteValidatorConfig; + handler: (args: CaseRouteHandlerArguments) => Promise; +} diff --git a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index 2e38ac8b4ebc79..b9b6ce43a9fdfd 100644 --- a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -7,50 +7,44 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../types'; -import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils'; +import { getWarningHeader, logDeprecatedEndpoint } from '../utils'; import { CASE_USER_ACTIONS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; /** * @deprecated since version 8.1.0 */ -export function initGetAllCaseUserActionsApi({ router, logger, kibanaVersion }: RouteDeps) { - router.get( - { - path: CASE_USER_ACTIONS_URL, - validate: { - params: schema.object({ - case_id: schema.string(), - }), - }, - }, - async (context, request, response) => { - try { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } +export const getUserActionsRoute = createCasesRoute({ + method: 'get', + path: CASE_USER_ACTIONS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response, logger, kibanaVersion }) => { + try { + logDeprecatedEndpoint( + logger, + request.headers, + `The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.` + ); - logDeprecatedEndpoint( - logger, - request.headers, - `The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.` - ); + const casesClient = await context.cases.getCasesClient(); + const caseId = request.params.case_id; - const casesClient = await context.cases.getCasesClient(); - const caseId = request.params.case_id; - - return response.ok({ - headers: { - ...getWarningHeader(kibanaVersion), - }, - body: await casesClient.userActions.getAll({ caseId }), - }); - } catch (error) { - logger.error( - `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}` - ); - return response.customError(wrapError(error)); - } + return response.ok({ + headers: { + ...getWarningHeader(kibanaVersion), + }, + body: await casesClient.userActions.getAll({ caseId }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}`, + error, + }); } - ); -} + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index 532a316e9a7b84..3536e4db346679 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -52,10 +52,10 @@ export const getWarningHeader = ( * https://github.com/elastic/kibana/blob/ec30f2aeeb10fb64b507935e558832d3ef5abfaa/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts#L113-L118 */ -const getIsKibanaRequest = (headers?: Headers) => { +export const getIsKibanaRequest = (headers?: Headers): boolean => { // The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. // We can't be 100% certain, but this is a reasonable attempt. - return headers && headers['kbn-version'] && headers.referer; + return !!(headers && headers['kbn-version'] && headers.referer); }; export const logDeprecatedEndpoint = (logger: Logger, headers: Headers, msg: string) => { diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 832d12071b4662..684edcc077f8e2 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -39,7 +39,7 @@ import { } from '../../../common/api'; import { SavedObjectFindOptionsKueryNode } from '../../common/types'; import { defaultSortField, flattenCaseSavedObject } from '../../common/utils'; -import { defaultPage, defaultPerPage } from '../../routes/api'; +import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { @@ -420,8 +420,8 @@ export class CasesService { return { saved_objects: [], total: 0, - per_page: options?.perPage ?? defaultPerPage, - page: options?.page ?? defaultPage, + per_page: options?.perPage ?? DEFAULT_PER_PAGE, + page: options?.page ?? DEFAULT_PAGE, }; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts index 303d54c9d45ccf..e7ad239e0c9801 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts @@ -6,7 +6,6 @@ */ import moment from 'moment'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { TimefilterContract } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; @@ -27,8 +26,7 @@ export async function setFullTimeRange( query?: QueryDslQueryContainer, excludeFrozenData?: boolean ): Promise { - const runtimeMappings = indexPattern.getComputedFields() - .runtimeFields as estypes.MappingRuntimeFields; + const runtimeMappings = indexPattern.getRuntimeMappings(); const resp = await getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts index a6176b8e3c1ce7..c4e9e898454d51 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -198,7 +198,7 @@ export const useDataVisualizerGridData = ( sessionId: searchSessionId, index: currentIndexPattern.title, timeFieldName: currentIndexPattern.timeFieldName, - runtimeFieldMap: currentIndexPattern.getComputedFields().runtimeFields, + runtimeFieldMap: currentIndexPattern.getRuntimeMappings(), aggregatableFields, nonAggregatableFields, fieldsToFetch, diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 5ff3b6c481d746..22898ac54db5ac 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -393,7 +393,9 @@ describe('setIndexToHidden', () => { expect(clusterClient.indices.putSettings).toHaveBeenCalledWith({ index: 'foo-bar-000001', body: { - 'index.hidden': true, + index: { + hidden: true, + }, }, }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 010d162c62ea13..bb958c3ce2b54f 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -178,7 +178,6 @@ export class ClusterClientAdapter; + +export interface PostLogstashApiKeyResponse { + api_key: string; +} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx index a8354237bbcb7c..655875d0448938 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx @@ -70,6 +70,11 @@ export const SelectCreateAgentPolicy: React.FC = ({ [onAgentPolicyChange] ); + const onClickCreatePolicy = () => { + setCreateState({ status: CREATE_STATUS.INITIAL }); + setShowCreatePolicy(true); + }; + return ( <> {showCreatePolicy ? ( @@ -86,7 +91,7 @@ export const SelectCreateAgentPolicy: React.FC = ({ onKeyChange={onKeyChange} onAgentPolicyChange={onAgentPolicyChange} excludeFleetServer={excludeFleetServer} - onClickCreatePolicy={() => setShowCreatePolicy(true)} + onClickCreatePolicy={onClickCreatePolicy} selectedAgentPolicy={selectedAgentPolicy} isFleetServerPolicy={isFleetServerPolicy} /> diff --git a/x-pack/plugins/fleet/server/routes/output/handler.ts b/x-pack/plugins/fleet/server/routes/output/handler.ts index 6de9fe1204f1c0..d4372c22f32de7 100644 --- a/x-pack/plugins/fleet/server/routes/output/handler.ts +++ b/x-pack/plugins/fleet/server/routes/output/handler.ts @@ -18,10 +18,12 @@ import type { DeleteOutputResponse, GetOneOutputResponse, GetOutputsResponse, + PostLogstashApiKeyResponse, } from '../../../common'; import { outputService } from '../../services/output'; -import { defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler, FleetUnauthorizedError } from '../../errors'; import { agentPolicyService } from '../../services'; +import { generateLogstashApiKey, canCreateLogstashApiKey } from '../../services/api_keys'; export const getOutputsHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; @@ -142,3 +144,24 @@ export const deleteOutputHandler: RequestHandler< return defaultIngestErrorHandler({ error, response }); } }; + +export const postLogstashApiKeyHandler: RequestHandler = async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { + const hasCreatePrivileges = await canCreateLogstashApiKey(esClient); + if (!hasCreatePrivileges) { + throw new FleetUnauthorizedError('Missing permissions to create logstash API key'); + } + + const apiKey = await generateLogstashApiKey(esClient); + + const body: PostLogstashApiKeyResponse = { + // Logstash expect the key to be formatted like this id:key + api_key: `${apiKey.id}:${apiKey.api_key}`, + }; + + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/output/index.ts b/x-pack/plugins/fleet/server/routes/output/index.ts index b9dfb1f7f742bf..f74c1bb88aeb03 100644 --- a/x-pack/plugins/fleet/server/routes/output/index.ts +++ b/x-pack/plugins/fleet/server/routes/output/index.ts @@ -21,6 +21,7 @@ import { getOutputsHandler, postOuputHandler, putOuputHandler, + postLogstashApiKeyHandler, } from './handler'; export const registerRoutes = (router: FleetAuthzRouter) => { @@ -76,4 +77,15 @@ export const registerRoutes = (router: FleetAuthzRouter) => { }, deleteOutputHandler ); + + router.post( + { + path: OUTPUT_API_ROUTES.LOGSTASH_API_KEY_PATTERN, + validate: false, + fleetAuthz: { + fleet: { all: true }, + }, + }, + postLogstashApiKeyHandler + ); }; diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index c781b2d01943fc..7b96d71c7ac9cd 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -5,36 +5,6 @@ * 2.0. */ -import type { KibanaRequest } from 'src/core/server'; - export { invalidateAPIKeys } from './security'; +export { generateLogstashApiKey, canCreateLogstashApiKey } from './logstash_api_keys'; export * from './enrollment_api_key'; - -export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { - const authorizationHeader = headers.authorization; - - if (!authorizationHeader) { - throw new Error('Authorization header must be set'); - } - - if (Array.isArray(authorizationHeader)) { - throw new Error('Authorization header must be `string` not `string[]`'); - } - - if (!authorizationHeader.startsWith('ApiKey ')) { - throw new Error('Authorization header is malformed'); - } - - const apiKey = authorizationHeader.split(' ')[1]; - - return parseApiKey(apiKey); -} - -export function parseApiKey(apiKey: string) { - const apiKeyId = Buffer.from(apiKey, 'base64').toString('utf8').split(':')[0]; - - return { - apiKey, - apiKeyId, - }; -} diff --git a/x-pack/plugins/fleet/server/services/api_keys/logstash_api_keys.ts b/x-pack/plugins/fleet/server/services/api_keys/logstash_api_keys.ts new file mode 100644 index 00000000000000..d55aa37fd5150d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/api_keys/logstash_api_keys.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; + +/** + * Check if an esClient has enought permission to create a valid API key for logstash + * + * @param esClient + */ +export async function canCreateLogstashApiKey(esClient: ElasticsearchClient) { + const res = await esClient.security.hasPrivileges({ + cluster: ['monitor', 'manage_own_api_key'], + index: [ + { + names: [ + 'logs-*-*', + 'metrics-*-*', + 'traces-*-*', + 'synthetics-*-*', + '.logs-endpoint.diagnostic.collection-*', + '.logs-endpoint.action.responses-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }); + + return res.has_all_requested; +} + +/** + * Generate an Elasticsearch API key to use in logstash ES output + * + * @param esClient + */ +export async function generateLogstashApiKey(esClient: ElasticsearchClient) { + const apiKey = await esClient.security.createApiKey({ + name: 'Fleet Logstash output', + metadata: { + managed_by: 'fleet', + managed: true, + type: 'logstash', + }, + role_descriptors: { + 'logstash-output': { + cluster: ['monitor'], + index: [ + { + names: [ + 'logs-*-*', + 'metrics-*-*', + 'traces-*-*', + 'synthetics-*-*', + '.logs-endpoint.diagnostic.collection-*', + '.logs-endpoint.action.responses-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }, + }); + + return apiKey; +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts index e5c96bea871818..52951377c6e2e7 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts @@ -74,13 +74,20 @@ async function handleMlModelInstall({ try { await retryTransientEsErrors( () => - esClient.ml.putTrainedModel({ - model_id: mlModel.installationName, - defer_definition_decompression: true, - timeout: '45s', - // @ts-expect-error expects an object not a string - body: mlModel.content, - }), + esClient.ml.putTrainedModel( + { + model_id: mlModel.installationName, + defer_definition_decompression: true, + timeout: '45s', + // @ts-expect-error expects an object not a string + body: mlModel.content, + }, + { + headers: { + 'content-type': 'application/json', + }, + } + ), { logger } ); } catch (err) { 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 5d144435bbee11..7650caf73d7145 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 @@ -714,42 +714,53 @@ describe('EPM template', () => { expect(mappings).toEqual(expectedMapping); }); - it('processes meta fields', () => { - const metaFieldLiteralYaml = ` -- name: fieldWithMetas - type: integer - unit: byte + it('tests processing metric_type field', () => { + const literalYml = ` +- name: total.norm.pct + type: scaled_float metric_type: gauge - `; - const metaFieldMapping = { + unit: percent + format: percent +`; + const expectedMapping = { properties: { - fieldWithMetas: { - type: 'long', - meta: { - metric_type: 'gauge', - unit: 'byte', + total: { + properties: { + norm: { + properties: { + pct: { + scaling_factor: 1000, + type: 'scaled_float', + meta: { + metric_type: 'gauge', + unit: 'percent', + }, + time_series_metric: 'gauge', + }, + }, + }, }, }, }, }; - const fields: Field[] = safeLoad(metaFieldLiteralYaml); + const fields: Field[] = safeLoad(literalYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(metaFieldMapping)); + expect(mappings).toEqual(expectedMapping); }); - it('processes meta fields with only one meta value', () => { + it('processes meta fields', () => { const metaFieldLiteralYaml = ` - name: fieldWithMetas type: integer - metric_type: gauge + unit: byte `; const metaFieldMapping = { properties: { fieldWithMetas: { type: 'long', meta: { - metric_type: 'gauge', + unit: 'byte', }, }, }, @@ -765,16 +776,13 @@ describe('EPM template', () => { - name: groupWithMetas type: group unit: byte - metric_type: gauge fields: - name: fieldA type: integer unit: byte - metric_type: gauge - name: fieldB type: integer unit: byte - metric_type: gauge `; const metaFieldMapping = { properties: { @@ -783,14 +791,12 @@ describe('EPM template', () => { fieldA: { type: 'long', meta: { - metric_type: 'gauge', unit: 'byte', }, }, fieldB: { type: 'long', meta: { - metric_type: 'gauge', unit: 'byte', }, }, 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 f88f5aeb1c727b..73ad218d1a9fd0 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 @@ -132,6 +132,9 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { case 'scaled_float': fieldProps.type = 'scaled_float'; fieldProps.scaling_factor = field.scaling_factor || DEFAULT_SCALING_FACTOR; + if (field.metric_type) { + fieldProps.time_series_metric = field.metric_type; + } break; case 'text': const textMapping = generateTextMapping(field); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts index 6a2284e0df742a..07748c1635b3a6 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts @@ -46,7 +46,6 @@ export const deleteTransforms = async (esClient: ElasticsearchClient, transformI await esClient.transport.request( { method: 'DELETE', - // @ts-expect-error @elastic/elasticsearch Transform is empty interface path: `/${transform?.dest?.index}`, }, { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 879c7614fedbf4..4a1909cc813e7a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -106,6 +106,7 @@ describe('test transform install', () => { esClient.transform.getTransform.mockResponseOnce({ count: 1, transforms: [ + // @ts-expect-error incomplete data { dest: { index: 'index', @@ -394,6 +395,7 @@ describe('test transform install', () => { esClient.transform.getTransform.mockResponseOnce({ count: 1, transforms: [ + // @ts-expect-error incomplete data { dest: { index: 'index', diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 1acbce4f22cd1d..546ae9c6fb9acf 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -28,6 +28,7 @@ import { doesAgentPolicyAlreadyIncludePackage, validatePackagePolicy, validationHasErrors, + SO_SEARCH_LIMIT, } from '../../common'; import type { DeletePackagePoliciesResponse, @@ -369,9 +370,10 @@ class PackagePolicyService implements PackagePolicyServiceInterface { } // Check that the name does not exist already but exclude the current package policy const existingPoliciesWithName = await this.list(soClient, { - perPage: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: "${packagePolicy.name}"`, + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name:"${packagePolicy.name}"`, }); + const filtered = (existingPoliciesWithName?.items || []).filter((p) => p.id !== id); if (filtered.length > 0) { diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index 9e8a8b23a7d9d0..cec763a247ed7e 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -51,9 +51,7 @@ async function fetchIndicesCall( const indexStats = indicesStats[indexName]; const aliases = Object.keys(indexData.aliases!); return { - // @ts-expect-error new property https://github.com/elastic/elasticsearch-specification/issues/1253 health: indexStats?.health, - // @ts-expect-error new property https://github.com/elastic/elasticsearch-specification/issues/1253 status: indexStats?.status, name: indexName, uuid: indexStats?.uuid, diff --git a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts index 03d3ec757bf552..46f961d6de4725 100644 --- a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts +++ b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts @@ -5,15 +5,17 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { from, of } from 'rxjs'; import { delay } from 'rxjs/operators'; import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; -import { fieldList, FieldSpec, RuntimeField } from '../../../../../src/plugins/data/common'; +import { fieldList, FieldSpec } from '../../../../../src/plugins/data/common'; type IndexPatternMock = Pick< DataView, | 'fields' | 'getComputedFields' + | 'getRuntimeMappings' | 'getFieldByName' | 'getTimeField' | 'id' @@ -23,6 +25,7 @@ type IndexPatternMock = Pick< >; type IndexPatternMockSpec = Pick & { fields: FieldSpec[]; + runtimeFields?: estypes.MappingRuntimeFields; }; export const createIndexPatternMock = ({ @@ -30,6 +33,7 @@ export const createIndexPatternMock = ({ title, type = undefined, fields, + runtimeFields, timeFieldName, }: IndexPatternMockSpec): IndexPatternMock => { const indexPatternFieldList = fieldList(fields); @@ -43,21 +47,12 @@ export const createIndexPatternMock = ({ isTimeBased: () => timeFieldName != null, getFieldByName: (fieldName) => indexPatternFieldList.find(({ name }) => name === fieldName), getComputedFields: () => ({ - runtimeFields: indexPatternFieldList.reduce>( - (accumulatedFields, { name, runtimeField }) => ({ - ...accumulatedFields, - ...(runtimeField != null - ? { - [name]: runtimeField, - } - : {}), - }), - {} - ), + runtimeFields: runtimeFields ?? {}, scriptFields: {}, storedFields: [], docvalueFields: [], }), + getRuntimeMappings: () => runtimeFields ?? {}, }; }; diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts index 70f8e1cebabfe5..914c55824373a4 100644 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts @@ -7,7 +7,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; -import { ObjectEntries } from '../utility_types'; import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../constants'; import { ResolveLogSourceConfigurationError } from './errors'; import { @@ -106,27 +105,5 @@ const resolveKibanaIndexPatternReference = async ( // this might take other sources of runtime fields into account in the future const resolveRuntimeMappings = (indexPattern: DataView): estypes.MappingRuntimeFields => { - const { runtimeFields } = indexPattern.getComputedFields(); - - const runtimeMappingsFromIndexPattern = ( - Object.entries(runtimeFields) as ObjectEntries - ).reduce( - (accumulatedMappings, [runtimeFieldName, runtimeFieldSpec]) => ({ - ...accumulatedMappings, - [runtimeFieldName]: { - type: runtimeFieldSpec.type, - ...(runtimeFieldSpec.script != null - ? { - script: { - lang: 'painless', // required in the es types - source: runtimeFieldSpec.script.source, - }, - } - : {}), - }, - }), - {} - ); - - return runtimeMappingsFromIndexPattern; + return indexPattern.getRuntimeMappings(); }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 33fe3c7af30c78..c0a475ea8029b6 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -34,7 +34,7 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: consumer: 'infrastructure', onClose: onCloseFlyout, canChangeTrigger: false, - alertTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + ruleTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, metadata: { options, nodeType, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index bee7f93a538be9..d7270aa0ef0e58 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -25,7 +25,7 @@ export const AlertFlyout = (props: Props) => { consumer: 'logs', onClose: onCloseFlyout, canChangeTrigger: false, - alertTypeId: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, + ruleTypeId: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, metadata: { isInternal: true, }, diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx index 9d467e1df7e36a..e0e9946b1c7898 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx @@ -32,7 +32,7 @@ export const AlertFlyout = ({ metric, nodeType, visible, setVisible }: Props) => consumer: 'infrastructure', onClose: onCloseFlyout, canChangeTrigger: false, - alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, + ruleTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, metadata: { metric, nodeType, diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index e7e4ade5257fc1..642b7dbf079c03 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -30,7 +30,7 @@ export const AlertFlyout = (props: Props) => { consumer: 'infrastructure', onClose: onCloseFlyout, canChangeTrigger: false, - alertTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID, + ruleTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID, metadata: { currentOptions: props.options, series: props.series, diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 66abef53f021e5..8aaac2f1b9a46c 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -6,10 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { flowRight } from 'lodash'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import useMount from 'react-use/lib/useMount'; +import { flowRight } from 'lodash'; import { findInventoryFields } from '../../../common/inventory_models'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LoadingPage } from '../../components/loading_page'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts index 744fd80134aec9..81c714b30cb0d9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts @@ -6,8 +6,8 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { difference, first, has, isNaN, isNumber, isObject, last, mapValues } from 'lodash'; import moment from 'moment'; +import { difference, first, has, isNaN, isNumber, isObject, last, mapValues } from 'lodash'; import { Aggregators, Comparator, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index 73f832e834c610..cfa9e84fb3651a 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -80,7 +80,6 @@ describe('LogEntries search strategy', () => { runtime_field: { type: 'keyword', script: { - lang: 'painless', source: 'emit("runtime value")', }, }, @@ -359,6 +358,14 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ searchable: true, }, ], + runtimeFields: { + runtime_field: { + type: 'keyword', + script: { + source: 'emit("runtime value")', + }, + }, + }, }), ]), }); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 20f3e41cef159a..c2f3c70580040f 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -79,7 +79,6 @@ describe('LogEntry search strategy', () => { runtime_field: { type: 'keyword', script: { - lang: 'painless', source: 'emit("runtime value")', }, }, @@ -314,6 +313,14 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ searchable: true, }, ], + runtimeFields: { + runtime_field: { + type: 'keyword', + script: { + source: 'emit("runtime value")', + }, + }, + }, }), ]), }); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx index 5f1230f004684a..d6a5b4e01a9b70 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx @@ -66,7 +66,7 @@ describe('', () => { expect(find('pageTitle').text()).toEqual('Create pipeline from CSV'); expect(exists('documentationLink')).toBe(true); - expect(find('documentationLink').text()).toBe('Create pipeline docs'); + expect(find('documentationLink').text()).toBe('CSV to pipeline docs'); }); describe('form validation', () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 097ec3d98e162b..a7fbf6afaebf83 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -85,7 +85,7 @@ export const PipelinesCreate: React.FunctionComponent , ]} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index a902f4a34af293..c927a324b0774d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -134,7 +134,7 @@ export const PipelinesEdit: React.FunctionComponent - { - const newMode = id.replace(idPrefix, '') as LineStyle; - setConfig({ forAccessor: accessor, lineStyle: newMode }); - }} - /> - - - { - setConfig({ forAccessor: accessor, lineWidth: value }); - }} - /> + + + { + setConfig({ forAccessor: accessor, lineWidth: value }); + }} + /> + + + { + const newMode = id.replace(idPrefix, '') as LineStyle; + setConfig({ forAccessor: accessor, lineStyle: newMode }); + }} + isIconOnly + /> + + ); @@ -108,11 +116,10 @@ const LineThicknessSlider = ({ const [unsafeValue, setUnsafeValue] = useState(String(value)); return ( - f.isScript); - const runtimeFields = fields.filter((f) => f.runtimeField); const result = await client.search( { index, @@ -242,11 +246,7 @@ async function fetchIndexPatternStats({ sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [], fields: ['*'], _source: false, - runtime_mappings: runtimeFields.reduce((acc, field) => { - if (!field.runtimeField) return acc; - acc[field.name] = field.runtimeField; - return acc; - }, {} as Record), + runtime_mappings: runtimeMappings, script_fields: scriptedFields.reduce((acc, field) => { acc[field.name] = { script: { diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 6c1a93759030a6..127d798cb76c2f 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -84,6 +84,7 @@ export async function initFieldsRoute(setup: CoreSetup) { .filter((f) => f.runtimeField) .reduce((acc, f) => { if (!f.runtimeField) return acc; + // @ts-expect-error The MappingRuntimeField from @elastic/elasticsearch does not expose the "composite" runtime type yet acc[f.name] = f.runtimeField; return acc; }, {} as Record); diff --git a/x-pack/plugins/license_management/server/lib/license.ts b/x-pack/plugins/license_management/server/lib/license.ts index 12f831f3d780a3..915d3a8b50a3d9 100644 --- a/x-pack/plugins/license_management/server/lib/license.ts +++ b/x-pack/plugins/license_management/server/lib/license.ts @@ -18,6 +18,7 @@ interface PutLicenseArg { export async function putLicense({ acknowledge, client, licensing, license }: PutLicenseArg) { try { const response = await client.asCurrentUser.license.post({ + // @ts-expect-error license is not typed in LM code body: license, acknowledge, }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 9079390cb301a4..206b1a5dd6f85f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -43,7 +43,7 @@ import { getEmptyValue } from '../../../common/empty_value'; import * as i18n from './translations'; -const MyValuesInput = styled(EuiFlexItem)` +const FieldFlexItem = styled(EuiFlexItem)` overflow: hidden; `; @@ -166,19 +166,22 @@ export const BuilderEntryItem: React.FC = ({ isDisabled={isDisabled || indexPattern == null} onChange={handleFieldChange} data-test-subj="exceptionBuilderEntryField" - fieldInputWidth={275} /> ); if (isFirst) { return ( - + {comboBox} ); } else { return ( - + {comboBox} ); @@ -319,14 +322,14 @@ export const BuilderEntryItem: React.FC = ({ className="exceptionItemEntryContainer" data-test-subj="exceptionItemEntryContainer" > - {renderFieldInput(showLabel)} + {renderFieldInput(showLabel)} {renderOperatorInput(showLabel)} - + {renderFieldValueInput( showLabel, entry.nested === 'parent' ? OperatorTypeEnum.EXISTS : entry.operator.type )} - + ); }; diff --git a/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.test.ts b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.test.ts new file mode 100644 index 00000000000000..51d7a41f797eb5 --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter } from '@kbn/es-query'; +import { migrateDataPersistedState } from './migrate_data_persisted_state'; + +const attributes = { + title: 'My map', + mapStateJSON: + '{"filters":[{"meta":{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","params":{"lt":10000,"gte":2000},"field":"bytes","alias":null,"negate":false,"disabled":false,"type":"range","key":"bytes"},"query":{"range":{"bytes":{"lt":10000,"gte":2000}}},"$state":{"store":"appState"}}]}', +}; + +const filterMigrationMock = (filters: Filter[]): Filter[] => { + return filters.map((filter) => { + return { + ...filter, + alias: 'filter_has_been_migrated', + }; + }); +}; + +test('should apply data migrations to data peristed data', () => { + const { mapStateJSON } = migrateDataPersistedState({ attributes }, filterMigrationMock); + const mapState = JSON.parse(mapStateJSON!); + expect(mapState.filters[0].alias).toEqual('filter_has_been_migrated'); +}); diff --git a/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts new file mode 100644 index 00000000000000..7a933663a60f24 --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.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 { Filter } from '@kbn/es-query'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import { MigrateFunction } from '../../../../../src/plugins/kibana_utils/common'; + +export function migrateDataPersistedState( + { + attributes, + }: { + attributes: MapSavedObjectAttributes; + }, + filterMigration: MigrateFunction +): MapSavedObjectAttributes { + let mapState: { filters: Filter[] } = { filters: [] }; + if (attributes.mapStateJSON) { + try { + mapState = JSON.parse(attributes.mapStateJSON); + } catch (e) { + throw new Error('Unable to parse attribute mapStateJSON'); + } + + mapState.filters = filterMigration(mapState.filters); + } + + return { + ...attributes, + mapStateJSON: JSON.stringify(mapState), + }; +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts index 5177cdb8148333..a9edef4cd15d89 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts @@ -20,7 +20,9 @@ export class StyleMeta { } getCategoryFieldMetaDescriptor(fieldName: string): Category[] { - return this._descriptor.fieldMeta[fieldName].categories; + return this._descriptor.fieldMeta[fieldName] + ? this._descriptor.fieldMeta[fieldName].categories + : []; } isPointsOnly(): boolean { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index f031b3cd221056..0e87651e234bcd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -521,9 +521,11 @@ export class VectorStyle implements IVectorStyle { if (!styleMeta.fieldMeta[name]) { styleMeta.fieldMeta[name] = { categories: [] }; } - styleMeta.fieldMeta[name].categories = + const categories = dynamicProperty.pluckCategoricalStyleMetaFromTileMetaFeatures(metaFeatures); - + if (categories.length) { + styleMeta.fieldMeta[name].categories = categories; + } const ordinalStyleMeta = dynamicProperty.pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures); if (ordinalStyleMeta) { @@ -601,8 +603,10 @@ export class VectorStyle implements IVectorStyle { if (!styleMeta.fieldMeta[name]) { styleMeta.fieldMeta[name] = { categories: [] }; } - styleMeta.fieldMeta[name].categories = - dynamicProperty.pluckCategoricalStyleMetaFromFeatures(features); + const categories = dynamicProperty.pluckCategoricalStyleMetaFromFeatures(features); + if (categories.length) { + styleMeta.fieldMeta[name].categories = categories; + } const ordinalStyleMeta = dynamicProperty.pluckOrdinalStyleMetaFromFeatures(features); if (ordinalStyleMeta) { styleMeta.fieldMeta[name].range = ordinalStyleMeta; diff --git a/x-pack/plugins/maps/server/embeddable_migrations.test.ts b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.test.ts similarity index 90% rename from x-pack/plugins/maps/server/embeddable_migrations.test.ts rename to x-pack/plugins/maps/server/embeddable/embeddable_migrations.test.ts index 4cf2642bb545c1..58a6716c517a9f 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.test.ts +++ b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.test.ts @@ -7,8 +7,7 @@ import semverGte from 'semver/functions/gte'; import { embeddableMigrations } from './embeddable_migrations'; -// @ts-ignore -import { savedObjectMigrations } from './saved_objects/saved_object_migrations'; +import { savedObjectMigrations } from '../saved_objects/saved_object_migrations'; describe('saved object migrations and embeddable migrations', () => { test('should have same versions registered (>7.12)', () => { diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts similarity index 84% rename from x-pack/plugins/maps/server/embeddable_migrations.ts rename to x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts index 9c17889e0c33c0..951877a31f8287 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts @@ -6,11 +6,11 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; -import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; -import { moveAttribution } from '../common/migrations/move_attribution'; -import { setEmsTmsDefaultModes } from '../common/migrations/set_ems_tms_default_modes'; -import { renameLayerTypes } from '../common/migrations/rename_layer_types'; -import { extractReferences } from '../common/migrations/references'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import { moveAttribution } from '../../common/migrations/move_attribution'; +import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; +import { renameLayerTypes } from '../../common/migrations/rename_layer_types'; +import { extractReferences } from '../../common/migrations/references'; /* * Embeddables such as Maps, Lens, and Visualize can be embedded by value or by reference on a dashboard. diff --git a/x-pack/plugins/maps/server/embeddable/index.ts b/x-pack/plugins/maps/server/embeddable/index.ts new file mode 100644 index 00000000000000..5061fc03dcfc14 --- /dev/null +++ b/x-pack/plugins/maps/server/embeddable/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 { setupEmbeddable } from './setup_embeddable'; diff --git a/x-pack/plugins/maps/server/embeddable/setup_embeddable.ts b/x-pack/plugins/maps/server/embeddable/setup_embeddable.ts new file mode 100644 index 00000000000000..9411869c5ad11a --- /dev/null +++ b/x-pack/plugins/maps/server/embeddable/setup_embeddable.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 { EmbeddableSetup } from '../../../../../src/plugins/embeddable/server'; +import { + mergeMigrationFunctionMaps, + MigrateFunctionsObject, +} from '../../../../../src/plugins/kibana_utils/common'; +import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { extract, inject } from '../../common/embeddable'; +import { embeddableMigrations } from './embeddable_migrations'; +import { getPersistedStateMigrations } from '../saved_objects'; + +export function setupEmbeddable( + embeddable: EmbeddableSetup, + getFilterMigrations: () => MigrateFunctionsObject +) { + embeddable.registerEmbeddableFactory({ + id: MAP_SAVED_OBJECT_TYPE, + migrations: () => { + return mergeMigrationFunctionMaps( + embeddableMigrations, + getPersistedStateMigrations(getFilterMigrations()) + ); + }, + inject, + extract, + }); +} diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 92d0f08fb51abb..05051a861d5956 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -22,15 +22,14 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants'; -import { extract, inject } from '../common/embeddable'; -import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; import { setStartServices } from './kibana_server_services'; import { emsBoundariesSpecProvider } from './tutorials/ems'; import { initRoutes } from './routes'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import type { EMSSettings } from '../../../../src/plugins/maps_ems/server'; -import { embeddableMigrations } from './embeddable_migrations'; +import { setupEmbeddable } from './embeddable'; +import { setupSavedObjects } from './saved_objects'; import { registerIntegrations } from './register_integrations'; import { StartDeps, SetupDeps } from './types'; @@ -146,6 +145,10 @@ export class MapsPlugin implements Plugin { } setup(core: CoreSetup, plugins: SetupDeps) { + const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind( + plugins.data.query.filterManager + ); + const { usageCollection, home, features, customIntegrations } = plugins; const config$ = this._initializerContext.config.create(); @@ -192,16 +195,10 @@ export class MapsPlugin implements Plugin { }, }); - core.savedObjects.registerType(mapsTelemetrySavedObjects); - core.savedObjects.registerType(mapSavedObjects); + setupSavedObjects(core, getFilterMigrations); registerMapsUsageCollector(usageCollection); - plugins.embeddable.registerEmbeddableFactory({ - id: MAP_SAVED_OBJECT_TYPE, - migrations: embeddableMigrations, - inject, - extract, - }); + setupEmbeddable(plugins.embeddable, getFilterMigrations); return { config: config$, diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index f34e20becd4372..1b0b129a292993 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { mapsTelemetrySavedObjects } from './maps_telemetry'; -export { mapSavedObjects } from './map'; +export { getPersistedStateMigrations, setupSavedObjects } from './setup_saved_objects'; diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts deleted file mode 100644 index b13f24fc6ba1cb..00000000000000 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, getFullPath } from '../../common/constants'; -// @ts-ignore -import { savedObjectMigrations } from './saved_object_migrations'; - -export const mapSavedObjects: SavedObjectsType = { - name: 'map', - hidden: false, - namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '8.0.0', - mappings: { - properties: { - description: { type: 'text' }, - title: { type: 'text' }, - version: { type: 'integer' }, - mapStateJSON: { type: 'text' }, - layerListJSON: { type: 'text' }, - uiStateJSON: { type: 'text' }, - bounds: { dynamic: false, properties: {} }, // Disable removed field - }, - }, - management: { - icon: APP_ICON, - defaultSearchField: 'title', - importableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getInAppUrl(obj) { - return { - path: getFullPath(obj.id), - uiCapabilitiesPath: 'maps.show', - }; - }, - }, - migrations: savedObjectMigrations, -}; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts deleted file mode 100644 index 35366188f909de..00000000000000 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsType } from 'src/core/server'; - -/* - * The maps-telemetry saved object type isn't used, but in order to remove these fields from - * the mappings we register this type with `type: 'object', enabled: true` to remove all - * previous fields from the mappings until https://github.com/elastic/kibana/issues/67086 is - * solved. - */ -export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps-telemetry', - hidden: false, - namespaceType: 'agnostic', - mappings: { - // @ts-ignore Core types don't support this since it's only really valid when removing a previously registered type - type: 'object', - enabled: false, - }, -}; diff --git a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts similarity index 71% rename from x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js rename to x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts index 986878e65eb8be..f8cd06cf60bfc3 100644 --- a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js +++ b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts @@ -5,11 +5,17 @@ * 2.0. */ +import type { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; import { extractReferences } from '../../common/migrations/references'; +// @ts-expect-error import { emsRasterTileToEmsVectorTile } from '../../common/migrations/ems_raster_tile_to_ems_vector_tile'; +// @ts-expect-error import { topHitsTimeToSort } from '../../common/migrations/top_hits_time_to_sort'; +// @ts-expect-error import { moveApplyGlobalQueryToSources } from '../../common/migrations/move_apply_global_query'; +// @ts-expect-error import { addFieldMetaOptions } from '../../common/migrations/add_field_meta_options'; +// @ts-expect-error import { migrateSymbolStyleDescriptor } from '../../common/migrations/migrate_symbol_style_descriptor'; import { migrateUseTopHitsToScalingType } from '../../common/migrations/scaling_type'; import { migrateJoinAggKey } from '../../common/migrations/join_agg_key'; @@ -19,8 +25,13 @@ import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin' import { moveAttribution } from '../../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; import { renameLayerTypes } from '../../common/migrations/rename_layer_types'; +import type { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; -function logMigrationWarning(context, errorMsg, doc) { +function logMigrationWarning( + context: SavedObjectMigrationContext, + errorMsg: string, + doc: SavedObjectUnsanitizedDoc +) { context.log.warning( `map migration failed (${context.migrationVersion}). ${errorMsg}. attributes: ${JSON.stringify( doc @@ -36,7 +47,10 @@ function logMigrationWarning(context, errorMsg, doc) { * This is the saved object migration registry. */ export const savedObjectMigrations = { - '7.2.0': (doc, context) => { + '7.2.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const { attributes, references } = extractReferences(doc); @@ -50,7 +64,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.4.0': (doc, context) => { + '7.4.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = emsRasterTileToEmsVectorTile(doc); @@ -63,7 +80,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.5.0': (doc, context) => { + '7.5.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = topHitsTimeToSort(doc); @@ -76,7 +96,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.6.0': (doc, context) => { + '7.6.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributesPhase1 = moveApplyGlobalQueryToSources(doc); const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); @@ -90,7 +113,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.7.0': (doc, context) => { + '7.7.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributesPhase1 = migrateSymbolStyleDescriptor(doc); const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); @@ -104,7 +130,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.8.0': (doc, context) => { + '7.8.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = migrateJoinAggKey(doc); @@ -117,7 +146,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.9.0': (doc, context) => { + '7.9.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = removeBoundsFromSavedObject(doc); @@ -130,7 +162,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.10.0': (doc, context) => { + '7.10.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = setDefaultAutoFitToBounds(doc); @@ -143,7 +178,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.12.0': (doc, context) => { + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = addTypeToTermJoin(doc); @@ -156,7 +194,10 @@ export const savedObjectMigrations = { return doc; } }, - '7.14.0': (doc, context) => { + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = moveAttribution(doc); @@ -169,7 +210,10 @@ export const savedObjectMigrations = { return doc; } }, - '8.0.0': (doc, context) => { + '8.0.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = setEmsTmsDefaultModes(doc); @@ -182,7 +226,10 @@ export const savedObjectMigrations = { return doc; } }, - '8.1.0': (doc, context) => { + '8.1.0': ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { try { const attributes = renameLayerTypes(doc); diff --git a/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts new file mode 100644 index 00000000000000..d5c0c0fa6dcf33 --- /dev/null +++ b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapValues } from 'lodash'; +import type { CoreSetup, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import type { SavedObjectMigrationMap } from 'src/core/server'; +import { MigrateFunctionsObject } from '../../../../../src/plugins/kibana_utils/common'; +import { mergeSavedObjectMigrationMaps } from '../../../../../src/core/server'; +import { APP_ICON, getFullPath } from '../../common/constants'; +import { migrateDataPersistedState } from '../../common/migrations/migrate_data_persisted_state'; +import type { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import { savedObjectMigrations } from './saved_object_migrations'; + +export function setupSavedObjects( + core: CoreSetup, + getFilterMigrations: () => MigrateFunctionsObject +) { + core.savedObjects.registerType({ + name: 'map', + hidden: false, + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', + mappings: { + properties: { + description: { type: 'text' }, + title: { type: 'text' }, + version: { type: 'integer' }, + mapStateJSON: { type: 'text' }, + layerListJSON: { type: 'text' }, + uiStateJSON: { type: 'text' }, + bounds: { dynamic: false, properties: {} }, // Disable removed field + }, + }, + management: { + icon: APP_ICON, + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: getFullPath(obj.id), + uiCapabilitiesPath: 'maps.show', + }; + }, + }, + migrations: () => { + return mergeSavedObjectMigrationMaps( + savedObjectMigrations, + getPersistedStateMigrations(getFilterMigrations()) as unknown as SavedObjectMigrationMap + ); + }, + }); + + /* + * The maps-telemetry saved object type isn't used, but in order to remove these fields from + * the mappings we register this type with `type: 'object', enabled: true` to remove all + * previous fields from the mappings until https://github.com/elastic/kibana/issues/67086 is + * solved. + */ + core.savedObjects.registerType({ + name: 'maps-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + // @ts-ignore Core types don't support this since it's only really valid when removing a previously registered type + type: 'object', + enabled: false, + }, + }); +} + +/** + * This creates a migration map that applies external plugin migrations to persisted state stored in Maps + */ +export const getPersistedStateMigrations = ( + filterMigrations: MigrateFunctionsObject +): MigrateFunctionsObject => + mapValues( + filterMigrations, + (filterMigration) => (doc: SavedObjectUnsanitizedDoc) => { + try { + const attributes = migrateDataPersistedState(doc, filterMigration); + + return { + ...doc, + attributes, + }; + } catch (e) { + // Do not fail migration + // Maps application can display error when saved object is viewed + return doc; + } + } + ); diff --git a/x-pack/plugins/maps/server/types.ts b/x-pack/plugins/maps/server/types.ts index 293964fdb6fee4..597b826eee56d8 100644 --- a/x-pack/plugins/maps/server/types.ts +++ b/x-pack/plugins/maps/server/types.ts @@ -11,10 +11,14 @@ import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { MapsEmsPluginServerSetup } from '../../../../src/plugins/maps_ems/server'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; import { CustomIntegrationsPluginSetup } from '../../../../src/plugins/custom_integrations/server'; export interface SetupDeps { + data: DataPluginSetup; features: FeaturesPluginSetupContract; usageCollection?: UsageCollectionSetup; home?: HomeServerPluginSetup; diff --git a/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx b/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx index b87a447bd4b155..d06cbbc02f6ed3 100644 --- a/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx +++ b/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx @@ -7,6 +7,8 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; + +import { Rule } from '../../../triggers_actions_ui/public'; import { JobId } from '../../common/types/anomaly_detection_jobs'; import { useMlKibana } from '../application/contexts/kibana'; import { ML_ALERT_TYPES } from '../../common/constants/alerts'; @@ -14,7 +16,7 @@ import { PLUGIN_ID } from '../../common/constants/app'; import { MlAnomalyDetectionAlertRule } from '../../common/types/alerts'; interface MlAnomalyAlertFlyoutProps { - initialAlert?: MlAnomalyDetectionAlertRule; + initialAlert?: MlAnomalyDetectionAlertRule & Rule; jobIds?: JobId[]; onSave?: () => void; onCloseFlyout: () => void; @@ -55,7 +57,10 @@ export const MlAnomalyAlertFlyout: FC = ({ if (initialAlert) { return triggersActionsUi.getEditAlertFlyout({ ...commonProps, - initialAlert, + initialRule: { + ...initialAlert, + ruleTypeId: initialAlert.ruleTypeId ?? initialAlert.alertTypeId, + }, }); } @@ -63,7 +68,7 @@ export const MlAnomalyAlertFlyout: FC = ({ ...commonProps, consumer: PLUGIN_ID, canChangeTrigger: false, - alertTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION, + ruleTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION, metadata: {}, initialValues: { params: { @@ -124,7 +129,7 @@ export const JobListMlAnomalyAlertFlyout: FC = }; interface EditRuleFlyoutProps { - initialAlert: MlAnomalyDetectionAlertRule; + initialAlert: MlAnomalyDetectionAlertRule & Rule; onSave: () => void; } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index ad790b75f0454d..790b69f7ebdd0e 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -19,7 +19,8 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { ): Promise { const intervalMs = Math.max( Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + (job.data_counts.latest_record_timestamp! - job.data_counts.earliest_record_timestamp!) / + bars ), bucketSpanMs ); @@ -27,8 +28,8 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { job.datafeed_config.indices.join(), job.datafeed_config.query, job.data_description.time_field!, - job.data_counts.earliest_record_timestamp, - job.data_counts.latest_record_timestamp, + job.data_counts.earliest_record_timestamp!, + job.data_counts.latest_record_timestamp!, intervalMs, job.datafeed_config.runtime_mappings, job.datafeed_config.indices_options @@ -60,15 +61,16 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { ) { const intervalMs = Math.max( Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + (job.data_counts.latest_record_timestamp! - job.data_counts.earliest_record_timestamp!) / + bars ), bucketSpanMs ); const resp = await mlResultsService.getScoresByBucket( [job.job_id], - job.data_counts.earliest_record_timestamp, - job.data_counts.latest_record_timestamp, + job.data_counts.earliest_record_timestamp!, + job.data_counts.latest_record_timestamp!, intervalMs, 1 ); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index b6b03f879bb57a..498a27834d050f 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -231,7 +231,7 @@ export const RevertModelSnapshotFlyout: FC = ({ overlayRanges={[ { start: currentSnapshot.latest_record_time_stamp, - end: job.data_counts.latest_record_timestamp, + end: job.data_counts.latest_record_timestamp!, color: '#ff0000', }, ]} @@ -334,7 +334,7 @@ export const RevertModelSnapshotFlyout: FC = ({ calendarEvents={calendarEvents} setCalendarEvents={setCalendarEvents} minSelectableTimeStamp={snapshot.latest_record_time_stamp} - maxSelectableTimeStamp={job.data_counts.latest_record_timestamp} + maxSelectableTimeStamp={job.data_counts.latest_record_timestamp!} eventRateData={eventRateData} anomalies={anomalies} chartReady={chartReady} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index 018fb326ba398b..b8b8db4c916aee 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -44,7 +44,10 @@ interface MLEuiDataGridColumn extends EuiDataGridColumn { function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { return Object.keys(runtimeMappings).map((id) => { - const field = runtimeMappings[id]; + let field = runtimeMappings[id]; + if (Array.isArray(field)) { + field = field[0]; + } const schema = getDataGridSchemaFromESFieldType( field.type as estypes.MappingRuntimeField['type'] ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 804a368174c76b..8c5a45137a8a12 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -262,6 +262,7 @@ export class JobCreator { this._initModelPlotConfig(); this._job_config.model_plot_config!.enabled = enable; } + public get modelPlot() { return ( this._job_config.model_plot_config !== undefined && @@ -737,7 +738,7 @@ export class JobCreator { ({ id, name: id, - type: runtimeField.type, + type: Array.isArray(runtimeField) ? runtimeField[0].type : runtimeField.type, aggregatable: true, aggs: [], runtimeField, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts index aba12ae93fdec9..c12f611c011f6d 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts @@ -34,6 +34,7 @@ const NODE_FIELDS = ['attributes', 'name', 'roles', 'version'] as const; export type RequiredNodeFields = Pick; +// @ts-expect-error TrainedModelDeploymentStatsResponse missing properties from MlTrainedModelDeploymentStats interface TrainedModelStatsResponse extends MlTrainedModelStats { deployment_stats?: Omit; model_size_stats?: TrainedModelModelSizeStats; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index dad1ecb7bc4baa..3df5016f560c07 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -603,8 +603,8 @@ export class DataRecognizer { } as JobStat; if (job.data_counts) { - jobStat.earliestTimestampMs = job.data_counts.earliest_record_timestamp; - jobStat.latestTimestampMs = job.data_counts.latest_record_timestamp; + jobStat.earliestTimestampMs = job.data_counts.earliest_record_timestamp!; + jobStat.latestTimestampMs = job.data_counts.latest_record_timestamp!; jobStat.latestResultsTimestampMs = getLatestDataOrBucketTimestamp( jobStat.latestTimestampMs, latestBucketTimestampsByJob[job.job_id] as number @@ -781,6 +781,7 @@ export class DataRecognizer { } private async _saveJob(job: ModuleJob) { + // @ts-expect-error type mismatch on MlPutJobRequest.body return this._mlClient.putJob({ job_id: job.id, body: job.config }); } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 81e1b1f6934493..2f8e17ce142a7a 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -590,7 +590,7 @@ export function jobsProvider( if (body.jobs.length) { const statsForJob = body.jobs[0]; - const time = statsForJob.data_counts.latest_record_timestamp; + const time = statsForJob.data_counts.latest_record_timestamp!; const progress = (time - start) / (end - start); const isJobClosed = statsForJob.state === JOB_STATE.CLOSED; return { @@ -631,6 +631,7 @@ export function jobsProvider( results[job.job_id] = { job: { success: false }, datafeed: { success: false } }; try { + // @ts-expect-error type mismatch on MlPutJobRequest.body await mlClient.putJob({ job_id: job.job_id, body: job }); results[job.job_id].job = { success: true }; } catch (error) { diff --git a/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts b/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts index e7bbc95ded7426..d7f6eb584f7fe0 100644 --- a/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts +++ b/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts @@ -74,7 +74,7 @@ export function memoryOverviewServiceProvider(mlClient: MlClient) { .filter((v) => v.state === 'opened') .map((jobStats) => { return { - node_id: jobStats.node.id, + node_id: jobStats.node!.id, // @ts-expect-error model_bytes can be string | number, cannot sum it with AD_PROCESS_MEMORY_OVERHEAD model_size: jobStats.model_size_stats.model_bytes + AD_PROCESS_MEMORY_OVERHEAD, job_id: jobStats.job_id, diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index ff58887c88c12e..9f06b452b9a18b 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -23,7 +23,7 @@ import { GenericValidationResult, RuleTypeModel, } from '../../../triggers_actions_ui/public/types'; -import { AlertForm } from '../../../triggers_actions_ui/public/application/sections/alert_form/alert_form'; +import { RuleForm } from '../../../triggers_actions_ui/public/application/sections/rule_form/rule_form'; import ActionForm from '../../../triggers_actions_ui/public/application/sections/action_connector_form/action_form'; import { Legacy } from '../legacy_shims'; import { I18nProvider } from '@kbn/i18n-react'; @@ -41,7 +41,7 @@ jest.mock('../../../triggers_actions_ui/public/application/lib/action_connector_ loadActionTypes: jest.fn(), })); -jest.mock('../../../triggers_actions_ui/public/application/lib/alert_api', () => ({ +jest.mock('../../../triggers_actions_ui/public/application/lib/rule_api', () => ({ loadAlertTypes: jest.fn(), })); @@ -117,7 +117,7 @@ describe('alert_form', () => { const initialAlert = { name: 'test', - alertTypeId: ruleType.id, + ruleTypeId: ruleType.id, params: {}, consumer: ALERTS_FEATURE_ID, schedule: { @@ -133,8 +133,8 @@ describe('alert_form', () => { wrapper = mountWithIntl( - {}} errors={{ name: [], interval: [] }} operation="create" @@ -152,13 +152,13 @@ describe('alert_form', () => { }); it('renders alert name', async () => { - const alertNameField = wrapper.find('[data-test-subj="alertNameInput"]'); + const alertNameField = wrapper.find('[data-test-subj="ruleNameInput"]'); expect(alertNameField.exists()).toBeTruthy(); expect(alertNameField.first().prop('value')).toBe('test'); }); it('renders registered selected alert type', async () => { - const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); + const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedRuleTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx index 5c5ce8c1673418..6dd62a9a1dae6e 100644 --- a/x-pack/plugins/monitoring/public/alerts/configuration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx @@ -86,7 +86,10 @@ export const AlertConfiguration: React.FC = (props: Props) => { () => showFlyout && Legacy.shims.triggersActionsUi.getEditAlertFlyout({ - initialAlert: alert, + initialRule: { + ...alert, + ruleTypeId: alert.alertTypeId, + }, onClose: () => { setShowFlyout(false); showBottomBar(); diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 82a0313ac711a8..89e5c937bacb61 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -17,7 +17,7 @@ import { observabilityFeatureId } from '../../../../../common'; import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { loadAlertAggregations as loadRuleAggregations } from '../../../../../../../plugins/triggers_actions_ui/public'; +import { loadRuleAggregations } from '../../../../../../../plugins/triggers_actions_ui/public'; import { AlertStatusFilterButton } from '../../../../../common/typings'; import { ParsedTechnicalFields } from '../../../../../../rule_registry/common/parse_technical_fields'; import { ParsedExperimentalFields } from '../../../../../../rule_registry/common/parse_experimental_fields'; @@ -107,13 +107,12 @@ function AlertsPage() { const response = await loadRuleAggregations({ http, }); - // Note that the API uses the semantics of 'alerts' instead of 'rules' - const { alertExecutionStatus, ruleMutedStatus, ruleEnabledStatus } = response; - if (alertExecutionStatus && ruleMutedStatus && ruleEnabledStatus) { - const total = Object.values(alertExecutionStatus).reduce((acc, value) => acc + value, 0); + const { ruleExecutionStatus, ruleMutedStatus, ruleEnabledStatus } = response; + if (ruleExecutionStatus && ruleMutedStatus && ruleEnabledStatus) { + const total = Object.values(ruleExecutionStatus).reduce((acc, value) => acc + value, 0); const { disabled } = ruleEnabledStatus; const { muted } = ruleMutedStatus; - const { error } = alertExecutionStatus; + const { error } = ruleExecutionStatus; setRuleStats({ ...ruleStats, total, diff --git a/x-pack/plugins/observability/server/utils/create_or_update_index.ts b/x-pack/plugins/observability/server/utils/create_or_update_index.ts index a9d583bbf86afd..af7dc670b0be80 100644 --- a/x-pack/plugins/observability/server/utils/create_or_update_index.ts +++ b/x-pack/plugins/observability/server/utils/create_or_update_index.ts @@ -77,7 +77,7 @@ function createNewIndex({ index, body: { // auto_expand_replicas: Allows cluster to not have replicas for this index - settings: { 'index.auto_expand_replicas': '0-1' }, + settings: { index: { auto_expand_replicas: '0-1' } }, mappings, }, }); diff --git a/x-pack/plugins/painless_lab/server/routes/api/execute.ts b/x-pack/plugins/painless_lab/server/routes/api/execute.ts index bc850f9e8043d5..58cb9f4328d297 100644 --- a/x-pack/plugins/painless_lab/server/routes/api/execute.ts +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -26,10 +26,17 @@ export function registerExecuteRoute({ router, license }: RouteDependencies) { try { const client = ctx.core.elasticsearch.client.asCurrentUser; - const response = await client.scriptsPainlessExecute({ - // @ts-expect-error `ExecutePainlessScriptRequest.body` does not allow `string` - body, - }); + const response = await client.scriptsPainlessExecute( + { + // @ts-expect-error `ExecutePainlessScriptRequest.body` does not allow `string` + body, + }, + { + headers: { + 'content-type': 'application/json', + }, + } + ); return res.ok({ body: response, diff --git a/x-pack/plugins/reporting/common/errors/index.ts b/x-pack/plugins/reporting/common/errors/index.ts index 3c0ade0ead7e3b..6064eca33ed7bc 100644 --- a/x-pack/plugins/reporting/common/errors/index.ts +++ b/x-pack/plugins/reporting/common/errors/index.ts @@ -7,6 +7,7 @@ /* eslint-disable max-classes-per-file */ +import { i18n } from '@kbn/i18n'; export abstract class ReportingError extends Error { public abstract code: string; @@ -45,6 +46,19 @@ export class UnknownError extends ReportingError { code = 'unknown_error'; } +export class PdfWorkerOutOfMemoryError extends ReportingError { + code = 'pdf_worker_out_of_memory_error'; + + details = i18n.translate('xpack.reporting.common.pdfWorkerOutOfMemoryErrorMessage', { + defaultMessage: + 'Cannot generate PDF due to low memory. Consider making a smaller PDF before retrying this report.', + }); + + public override get message(): string { + return this.details; + } +} + // TODO: Add ReportingError for Kibana stopping unexpectedly // TODO: Add ReportingError for missing Chromium dependencies // TODO: Add ReportingError for missing Chromium dependencies diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts index f8fefd790aa3b1..42845297e204e8 100644 --- a/x-pack/plugins/reporting/common/types/index.ts +++ b/x-pack/plugins/reporting/common/types/index.ts @@ -32,6 +32,7 @@ export interface ReportDocumentHead { export interface ReportOutput extends TaskRunResult { content: string | null; + error_code?: string; size: number; } @@ -71,7 +72,6 @@ export interface ReportSource { */ jobtype: string; // refers to `ExportTypeDefinition.jobType` created_by: string | false; // username or `false` if security is disabled. Used for ensuring users can only access the reports they've created. - error_code?: string; payload: BasePayload; meta: { // for telemetry diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index 1f7f2e21a45a27..e4a1680a958dd2 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -6,4 +6,3 @@ */ export { PdfMaker } from './pdfmaker'; -export { PdfWorkerOutOfMemoryError } from './pdfmaker_errors'; diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts index 4b35e0221685be..5302ba07f60020 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts @@ -10,8 +10,8 @@ import path from 'path'; import { isUint8Array } from 'util/types'; import { createMockLayout } from '../../../../../../screenshotting/server/layouts/mock'; +import { PdfWorkerOutOfMemoryError } from '../../../../../common/errors'; import { PdfMaker } from '../'; -import { PdfWorkerOutOfMemoryError } from '../pdfmaker_errors'; const imageBase64 = Buffer.from( `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`, diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts index 18e5346f71c40d..6685cf692ef59a 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts @@ -10,6 +10,7 @@ import path from 'path'; import { Content, ContentImage, ContentText } from 'pdfmake/interfaces'; import { MessageChannel, MessagePort, Worker } from 'worker_threads'; import type { Layout } from '../../../../../screenshotting/server'; +import { PdfWorkerOutOfMemoryError } from '../../../../common/errors'; import { headingHeight, pageMarginBottom, @@ -20,7 +21,6 @@ import { } from './constants'; import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; -import { PdfWorkerOutOfMemoryError } from './pdfmaker_errors'; import type { GeneratePdfRequest, GeneratePdfResponse, WorkerData } from './worker'; // Ensure that all dependencies are included in the release bundle. diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 459887ebb81187..a401f59b8f4bf0 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -13,7 +13,7 @@ import type { PdfMetrics } from '../../../../common/types'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; -import { PdfMaker, PdfWorkerOutOfMemoryError } from '../../common/pdf'; +import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { @@ -96,14 +96,7 @@ export function generatePdfObservable( tracker.setByteLength(byteLength); } catch (err) { logger.error(`Could not generate the PDF buffer!`); - logger.error(err); - if (err instanceof PdfWorkerOutOfMemoryError) { - warnings.push( - 'Failed to generate PDF due to low memory. Please consider generating a smaller PDF.' - ); - } else { - warnings.push(`Failed to generate PDF due to the following error: ${err.message}`); - } + throw err; } tracker.end(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index b0ac1a59010a69..ac922c07574b3c 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -14,7 +14,6 @@ import { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../com import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; -import { PdfWorkerOutOfMemoryError } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; import { getTracker } from './tracker'; @@ -109,14 +108,7 @@ export function generatePdfObservable( tracker.end(); } catch (err) { logger.error(`Could not generate the PDF buffer!`); - logger.error(err); - if (err instanceof PdfWorkerOutOfMemoryError) { - warnings.push( - 'Failed to generate PDF due to low memory. Please consider generating a smaller PDF.' - ); - } else { - warnings.push(`Failed to generate PDF due to the following error: ${err.message}`); - } + throw err; } return { diff --git a/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts index a7b9ecc7dc437c..c3aea64171444c 100644 --- a/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts +++ b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts @@ -29,6 +29,7 @@ export const checkIlmMigrationStatus = async ({ const hasUnmanagedIndices = Object.values(reportingIndicesSettings).some((settings) => { return ( settings?.settings?.index?.lifecycle?.name !== ILM_POLICY_NAME && + // @ts-expect-error index.lifecycle not present on type def settings?.settings?.['index.lifecycle']?.name !== ILM_POLICY_NAME ); }); diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts index db088aa99f1e12..9accfd0c751846 100644 --- a/x-pack/plugins/reporting/server/lib/store/mapping.ts +++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts @@ -44,7 +44,6 @@ export const mapping = { created_at: { type: 'date' }, started_at: { type: 'date' }, completed_at: { type: 'date' }, - error_code: { type: 'keyword' }, attempts: { type: 'short' }, max_attempts: { type: 'short' }, kibana_name: { type: 'keyword' }, @@ -54,6 +53,7 @@ export const mapping = { output: { type: 'object', properties: { + error_code: { type: 'keyword' }, chunk: { type: 'long' }, content_type: { type: 'keyword' }, size: { type: 'long' }, diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 29f61d8b2a364b..41fdd9580c996c 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -34,7 +34,6 @@ export type ReportProcessingFields = Required<{ export type ReportFailedFields = Required<{ completed_at: Report['completed_at']; output: ReportOutput | null; - error_code: undefined | string; }>; export type ReportCompletedFields = Required<{ diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 47f4e8617a9f37..019f128e5f07dd 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -211,7 +211,6 @@ export class ExecuteReportTask implements ReportingTask { const doc: ReportFailedFields = { completed_at: completedTime, output: docOutput ?? null, - error_code: error?.code, }; return await store.setReportFailed(report, doc); @@ -233,6 +232,7 @@ export class ExecuteReportTask implements ReportingTask { docOutput.content = output.toString() || defaultOutput; docOutput.content_type = unknownMime; docOutput.warnings = [output.details ?? output.toString()]; + docOutput.error_code = output.code; } return docOutput; @@ -369,7 +369,7 @@ export class ExecuteReportTask implements ReportingTask { report._primary_term = stream.getPrimaryTerm()!; eventLog.logExecutionComplete({ - ...(report.metrics ?? {}), + ...(output.metrics ?? {}), byteSize: stream.bytesWritten, }); @@ -416,12 +416,13 @@ export class ExecuteReportTask implements ReportingTask { if (report == null) { throw new Error(`Report ${jobId} is null!`); } - const maxAttemptsMsg = `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr.message}`; const error = failedToExecuteErr instanceof ReportingError ? failedToExecuteErr : new UnknownError(); - error.details = maxAttemptsMsg; + error.details = + error.details || + `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr.message}`; const resp = await this._failJob(report, error); report._seq_no = resp._seq_no; report._primary_term = resp._primary_term; diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts index b369a5758fcb5d..4c368337cd4822 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts @@ -126,8 +126,10 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log await client.indices.putSettings({ index: indexPattern, body: { - 'index.lifecycle': { - name: ILM_POLICY_NAME, + index: { + lifecycle: { + name: ILM_POLICY_NAME, + }, }, }, }); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index a13bf4936a77f5..ca067d08339811 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -35,6 +35,7 @@ export const registerCreateRoute = ({ // Create job. await clusterClient.asCurrentUser.rollup.putJob({ id, + // @ts-expect-error type mismatch on RollupPutJobRequest.body body: rest, }); // Then request the newly created job. diff --git a/x-pack/plugins/rule_registry/common/constants.ts b/x-pack/plugins/rule_registry/common/constants.ts index 72793b1087e7b3..1c5fad0e2215fd 100644 --- a/x-pack/plugins/rule_registry/common/constants.ts +++ b/x-pack/plugins/rule_registry/common/constants.ts @@ -6,3 +6,4 @@ */ export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; +export const MAX_ALERT_SEARCH_SIZE = 1000; diff --git a/x-pack/plugins/rule_registry/common/index.ts b/x-pack/plugins/rule_registry/common/index.ts index 5d36cd8cad7be2..2dd7f6bbc456e9 100644 --- a/x-pack/plugins/rule_registry/common/index.ts +++ b/x-pack/plugins/rule_registry/common/index.ts @@ -4,4 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export { parseTechnicalFields } from './parse_technical_fields'; +export { parseTechnicalFields, type ParsedTechnicalFields } from './parse_technical_fields'; +export type { RuleRegistrySearchRequest, RuleRegistrySearchResponse } from './search_strategy'; +export { BASE_RAC_ALERTS_API_PATH } from './constants'; diff --git a/x-pack/plugins/rule_registry/common/search_strategy/index.ts b/x-pack/plugins/rule_registry/common/search_strategy/index.ts new file mode 100644 index 00000000000000..efb8a3478263ea --- /dev/null +++ b/x-pack/plugins/rule_registry/common/search_strategy/index.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 { ValidFeatureId } from '@kbn/rule-data-utils'; +import { Ecs } from 'kibana/server'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IEsSearchRequest, IEsSearchResponse } from 'src/plugins/data/common'; + +export type RuleRegistrySearchRequest = IEsSearchRequest & { + featureIds: ValidFeatureId[]; + query?: { bool: estypes.QueryDslBoolQuery }; +}; + +type Prev = [ + never, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ...Array<0> +]; + +type Join = K extends string | number + ? P extends string | number + ? `${K}${'' extends P ? '' : '.'}${P}` + : never + : never; + +type DotNestedKeys = [D] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ''; + +type EcsFieldsResponse = { + [Property in DotNestedKeys]: string[]; +}; +export type RuleRegistrySearchResponse = IEsSearchResponse; diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 75e0c2c8c0bac4..9603cb0a2640bd 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -8,6 +8,6 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "ruleRegistry"], "requiredPlugins": ["alerting", "data", "triggersActionsUi"], - "optionalPlugins": ["security"], + "optionalPlugins": ["security", "spaces"], "server": true } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 1f8cfb4b78c858..a97b43332e0a9e 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -21,7 +21,7 @@ import { InlineScript, QueryDslQueryContainer, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { AlertTypeParams, AlertingAuthorizationFilterType } from '../../../alerting/server'; +import { AlertTypeParams } from '../../../alerting/server'; import { ReadOperations, AlertingAuthorization, @@ -39,6 +39,7 @@ import { } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; import { Dataset, IRuleDataService } from '../rule_data_plugin_service'; +import { getAuthzFilter, getSpacesFilter } from '../lib'; // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & { @@ -369,14 +370,8 @@ export class AlertsClient { config: EsQueryConfig ) { try { - const { filter: authzFilter } = await this.authorization.getAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.ESDSL, - fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, - }, - operation - ); + const authzFilter = (await getAuthzFilter(this.authorization, operation)) as Filter; + const spacesFilter = getSpacesFilter(alertSpaceId) as unknown as Filter; let esQuery; if (id != null) { esQuery = { query: `_id:${id}`, language: 'kuery' }; @@ -388,10 +383,7 @@ export class AlertsClient { const builtQuery = buildEsQuery( undefined, esQuery == null ? { query: ``, language: 'kuery' } : esQuery, - [ - authzFilter as unknown as Filter, - { query: { term: { [SPACE_IDS]: alertSpaceId } } } as unknown as Filter, - ], + [authzFilter, spacesFilter], config ); if (query != null && typeof query === 'object') { diff --git a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts new file mode 100644 index 00000000000000..3b79c7a5bad8a7 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.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 { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { ReadOperations } from '../../../alerting/server'; +import { getAuthzFilter } from './get_authz_filter'; + +describe('getAuthzFilter()', () => { + it('should call `getAuthorizationFilter`', async () => { + const authorization = alertingAuthorizationMock.create(); + authorization.getAuthorizationFilter.mockImplementationOnce(async () => { + return { filter: { test: true }, ensureRuleTypeIsAuthorized: () => {} }; + }); + const filter = await getAuthzFilter(authorization, ReadOperations.Find); + expect(filter).toStrictEqual({ test: true }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts new file mode 100644 index 00000000000000..88b8feb2ca97cf --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.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 { PublicMethodsOf } from '@kbn/utility-types'; +import { + ReadOperations, + WriteOperations, + AlertingAuthorization, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../../alerting/server'; +import { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, +} from '../../common/technical_rule_data_field_names'; + +export async function getAuthzFilter( + authorization: PublicMethodsOf, + operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find +) { + const { filter } = await authorization.getAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, + }, + operation + ); + return filter; +} diff --git a/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts new file mode 100644 index 00000000000000..7fd5f00fd99b19 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.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 { getSpacesFilter } from '.'; +describe('getSpacesFilter()', () => { + it('should return a spaces filter', () => { + expect(getSpacesFilter('1')).toStrictEqual({ + term: { + 'kibana.space_ids': '1', + }, + }); + }); + + it('should return undefined if no space id is provided', () => { + expect(getSpacesFilter()).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/header_page/types.ts b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts similarity index 51% rename from x-pack/plugins/cases/public/components/header_page/types.ts rename to x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts index e95d0c8e1e69c4..2756b3d600f18b 100644 --- a/x-pack/plugins/cases/public/components/header_page/types.ts +++ b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts @@ -4,17 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { SPACE_IDS } from '../../common/technical_rule_data_field_names'; -import type React from 'react'; -export type TitleProp = string | React.ReactNode; - -export interface DraggableArguments { - field: string; - value: string; -} - -export interface BadgeOptions { - beta?: boolean; - text: string; - tooltip?: string; +export function getSpacesFilter(spaceId?: string) { + return spaceId ? { term: { [SPACE_IDS]: spaceId } } : undefined; } diff --git a/x-pack/plugins/rule_registry/server/lib/index.ts b/x-pack/plugins/rule_registry/server/lib/index.ts new file mode 100644 index 00000000000000..c9ed157e7c18ab --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/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 { getAuthzFilter } from './get_authz_filter'; +export { getSpacesFilter } from './get_spaces_filter'; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 713e7862207b87..292e987879d58f 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -17,6 +17,11 @@ import { import { PluginStartContract as AlertingStart } from '../../alerting/server'; import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginStart } from '../../spaces/server'; +import { + PluginStart as DataPluginStart, + PluginSetup as DataPluginSetup, +} from '../../../../src/plugins/data/server'; import { RuleRegistryPluginConfig } from './config'; import { IRuleDataService, RuleDataService } from './rule_data_plugin_service'; @@ -24,13 +29,17 @@ import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; import { AlertsClient } from './alert_data_client/alerts_client'; import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; import { defineRoutes } from './routes'; +import { ruleRegistrySearchStrategyProvider } from './search_strategy'; export interface RuleRegistryPluginSetupDependencies { security?: SecurityPluginSetup; + data: DataPluginSetup; } export interface RuleRegistryPluginStartDependencies { alerting: AlertingStart; + data: DataPluginStart; + spaces?: SpacesPluginStart; } export interface RuleRegistryPluginSetupContract { @@ -95,6 +104,22 @@ export class RuleRegistryPlugin this.ruleDataService.initializeService(); + core.getStartServices().then(([_, depsStart]) => { + const ruleRegistrySearchStrategy = ruleRegistrySearchStrategyProvider( + depsStart.data, + this.ruleDataService!, + depsStart.alerting, + logger, + plugins.security, + depsStart.spaces + ); + + plugins.data.search.registerSearchStrategy( + 'ruleRegistryAlertsSearchStrategy', + ruleRegistrySearchStrategy + ); + }); + // ALERTS ROUTES const router = core.http.createRouter(); core.http.registerRouteHandlerContext( diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 2bda23ca3c46f8..8e7d13b0dc210d 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -309,10 +309,9 @@ export class ResourceInstaller { template: { settings: { hidden: true, + // @ts-expect-error type only defines nested structure 'index.lifecycle': { name: ilmPolicyName, - // TODO: fix the types in the ES package, they don't include rollover_alias??? - // @ts-expect-error rollover_alias: primaryNamespacedAlias, }, 'index.mapping.total_fields.limit': 1700, diff --git a/x-pack/plugins/rule_registry/server/search_strategy/index.ts b/x-pack/plugins/rule_registry/server/search_strategy/index.ts new file mode 100644 index 00000000000000..63f39430a55224 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/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 { ruleRegistrySearchStrategyProvider } from './search_strategy'; diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts new file mode 100644 index 00000000000000..9f83930dadc69b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { of } from 'rxjs'; +import { merge } from 'lodash'; +import { loggerMock } from '@kbn/logging-mocks'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { ruleRegistrySearchStrategyProvider, EMPTY_RESPONSE } from './search_strategy'; +import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; +import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server'; +import { alertsMock } from '../../../alerting/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { spacesMock } from '../../../spaces/server/mocks'; +import { RuleRegistrySearchRequest } from '../../common/search_strategy'; +import { IndexInfo } from '../rule_data_plugin_service/index_info'; +import * as getAuthzFilterImport from '../lib/get_authz_filter'; + +const getBasicResponse = (overwrites = {}) => { + return merge( + { + isPartial: false, + isRunning: false, + total: 0, + loaded: 0, + rawResponse: { + took: 1, + timed_out: false, + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + hits: { + max_score: 0, + hits: [], + total: 0, + }, + }, + }, + overwrites + ); +}; + +describe('ruleRegistrySearchStrategyProvider()', () => { + const data = dataPluginMock.createStartContract(); + const ruleDataService = ruleDataServiceMock.create(); + const alerting = alertsMock.createStart(); + const security = securityMock.createSetup(); + const spaces = spacesMock.createStart(); + const logger = loggerMock.create(); + + const response = getBasicResponse({ + rawResponse: { + hits: { + hits: [ + { + _source: { + foo: 1, + }, + }, + ], + }, + }, + }); + + let getAuthzFilterSpy: jest.SpyInstance; + + beforeEach(() => { + ruleDataService.findIndicesByFeature.mockImplementation(() => { + return [ + { + baseName: 'test', + } as IndexInfo, + ]; + }); + + data.search.getSearchStrategy.mockImplementation(() => { + return { + search: () => of(response), + }; + }); + + getAuthzFilterSpy = jest + .spyOn(getAuthzFilterImport, 'getAuthzFilter') + .mockImplementation(async () => { + return {}; + }); + }); + + afterEach(() => { + ruleDataService.findIndicesByFeature.mockClear(); + data.search.getSearchStrategy.mockClear(); + getAuthzFilterSpy.mockClear(); + }); + + it('should handle a basic search request', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.LOGS], + }; + const options = {}; + const deps = { + request: {}, + }; + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + const result = await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + expect(result).toBe(response); + }); + + it('should use the active space in siem queries', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.SIEM], + }; + const options = {}; + const deps = { + request: {}, + }; + + spaces.spacesService.getActiveSpace.mockImplementation(async () => { + return { + id: 'testSpace', + name: 'Test Space', + disabledFeatures: [], + }; + }); + + ruleDataService.findIndicesByFeature.mockImplementation(() => { + return [ + { + baseName: 'myTestIndex', + } as unknown as IndexInfo, + ]; + }); + + let searchRequest: RuleRegistrySearchRequest = {} as unknown as RuleRegistrySearchRequest; + data.search.getSearchStrategy.mockImplementation(() => { + return { + search: (_request) => { + searchRequest = _request as unknown as RuleRegistrySearchRequest; + return of(response); + }, + }; + }); + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + spaces.spacesService.getActiveSpace.mockClear(); + expect(searchRequest?.params?.index).toStrictEqual(['myTestIndex-testSpace*']); + }); + + it('should return an empty response if no valid indices are found', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.LOGS], + }; + const options = {}; + const deps = { + request: {}, + }; + + ruleDataService.findIndicesByFeature.mockImplementationOnce(() => { + return []; + }); + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + const result = await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + expect(result).toBe(EMPTY_RESPONSE); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts new file mode 100644 index 00000000000000..dd7f392b0a2684 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { map, mergeMap, catchError } from 'rxjs/operators'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Logger } from 'src/core/server'; +import { from, of } from 'rxjs'; +import { isValidFeatureId } from '@kbn/rule-data-utils'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common'; +import { ISearchStrategy, PluginStart } from '../../../../../src/plugins/data/server'; +import { + RuleRegistrySearchRequest, + RuleRegistrySearchResponse, +} from '../../common/search_strategy'; +import { ReadOperations, PluginStartContract as AlertingStart } from '../../../alerting/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { SpacesPluginStart } from '../../../spaces/server'; +import { IRuleDataService } from '..'; +import { Dataset } from '../rule_data_plugin_service/index_options'; +import { MAX_ALERT_SEARCH_SIZE } from '../../common/constants'; +import { AlertAuditAction, alertAuditEvent } from '../'; +import { getSpacesFilter, getAuthzFilter } from '../lib'; + +export const EMPTY_RESPONSE: RuleRegistrySearchResponse = { + rawResponse: {} as RuleRegistrySearchResponse['rawResponse'], +}; + +export const ruleRegistrySearchStrategyProvider = ( + data: PluginStart, + ruleDataService: IRuleDataService, + alerting: AlertingStart, + logger: Logger, + security?: SecurityPluginSetup, + spaces?: SpacesPluginStart +): ISearchStrategy => { + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return { + search: (request, options, deps) => { + const securityAuditLogger = security?.audit.asScoped(deps.request); + const getActiveSpace = async () => spaces?.spacesService.getActiveSpace(deps.request); + const getAsync = async () => { + const [space, authorization] = await Promise.all([ + getActiveSpace(), + alerting.getAlertingAuthorizationWithRequest(deps.request), + ]); + const authzFilter = (await getAuthzFilter( + authorization, + ReadOperations.Find + )) as estypes.QueryDslQueryContainer; + return { space, authzFilter }; + }; + return from(getAsync()).pipe( + mergeMap(({ space, authzFilter }) => { + const indices: string[] = request.featureIds.reduce((accum: string[], featureId) => { + if (!isValidFeatureId(featureId)) { + logger.warn( + `Found invalid feature '${featureId}' while using rule registry search strategy. No alert data from this feature will be searched.` + ); + return accum; + } + + return [ + ...accum, + ...ruleDataService + .findIndicesByFeature(featureId, Dataset.alerts) + .map((indexInfo) => { + return featureId === 'siem' + ? `${indexInfo.baseName}-${space?.id ?? ''}*` + : `${indexInfo.baseName}*`; + }), + ]; + }, []); + + if (indices.length === 0) { + return of(EMPTY_RESPONSE); + } + + const filter = request.query?.bool?.filter + ? Array.isArray(request.query?.bool?.filter) + ? request.query?.bool?.filter + : [request.query?.bool?.filter] + : []; + if (authzFilter) { + filter.push(authzFilter); + } + if (space?.id) { + filter.push(getSpacesFilter(space.id) as estypes.QueryDslQueryContainer); + } + + const query = { + bool: { + ...request.query?.bool, + filter, + }, + }; + const params = { + index: indices, + body: { + _source: false, + fields: ['*'], + size: MAX_ALERT_SEARCH_SIZE, + query, + }, + }; + return es.search({ ...request, params }, options, deps); + }), + map((response) => { + // Do we have to loop over each hit? Yes. + // ecs auditLogger requires that we log each alert independently + if (securityAuditLogger != null) { + response.rawResponse.hits?.hits?.forEach((hit) => { + securityAuditLogger.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + id: hit._id, + outcome: 'success', + }) + ); + }); + } + return response; + }), + catchError((err) => { + // check if auth error, if yes, write to ecs logger + if (securityAuditLogger != null && err?.output?.statusCode === 403) { + securityAuditLogger.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + outcome: 'failure', + error: err, + }) + ); + } + + throw err; + }) + ); + }, + cancel: async (id, options, deps) => { + if (es.cancel) { + return es.cancel(id, options, deps); + } + }, + }; +}; diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 384ffa0ee34281..810524a7a8122e 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -19,6 +19,5 @@ { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../security/tsconfig.json" }, - { "path": "../triggers_actions_ui/tsconfig.json" } ] } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts index f15e3f418427a9..90bd928cbd1fe3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts @@ -17,7 +17,7 @@ import { } from '@kbn/securitysolution-list-constants'; import { BaseDataGenerator } from './base_data_generator'; import { ConditionEntryField } from '../types'; -import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../service/artifacts/constants'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from '../service/artifacts/constants'; /** Utility that removes null and undefined from a Type's property value */ type NonNullableTypeProperties = { @@ -87,6 +87,11 @@ const exceptionItemToUpdateExceptionItem = ( }; }; +const EFFECTIVE_SCOPE: readonly string[] = [ + `${BY_POLICY_ARTIFACT_TAG_PREFIX}123-456`, // Policy Specific + GLOBAL_ARTIFACT_TAG, +]; + export class ExceptionsListItemGenerator extends BaseDataGenerator { generate(overrides: Partial = {}): ExceptionListItemSchema { const exceptionItem: ExceptionListItemSchema = { @@ -110,7 +115,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = [ + `name`, + `description`, + `entries.value`, + `entries.entries.value`, + `item_id`, +]; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts new file mode 100644 index 00000000000000..75076e191dcdc9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants'; +import { + createExceptionListItemForCreate, + getPolicyIdsFromArtifact, + isArtifactByPolicy, + isArtifactGlobal, +} from './utils'; + +describe('Endpoint artifact utilities', () => { + let globalEntry: Pick; + let perPolicyWithPolicy: Pick; + let perPolicyNoPolicies: Pick; + + beforeEach(() => { + globalEntry = { + tags: [GLOBAL_ARTIFACT_TAG], + }; + + perPolicyWithPolicy = { + tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`, `${BY_POLICY_ARTIFACT_TAG_PREFIX}456`], + }; + + perPolicyNoPolicies = { + tags: [], + }; + }); + + describe('when using `isArtifactGlobal()', () => { + it('should return `true` if artifact is global', () => { + expect(isArtifactGlobal(globalEntry)).toBe(true); + }); + + it('should return `false` if artifact is per-policy', () => { + expect(isArtifactGlobal(perPolicyWithPolicy)).toBe(false); + }); + + it('should return `false` if artifact is per-policy but not assigned to any policy', () => { + expect(isArtifactGlobal(perPolicyNoPolicies)).toBe(false); + }); + }); + + describe('when using `isArtifactByPolicy()', () => { + it('should return `true` if artifact is per-policy', () => { + expect(isArtifactByPolicy(perPolicyWithPolicy)).toBe(true); + }); + + it('should return `true` if artifact is per-policy but not assigned to any policy', () => { + expect(isArtifactByPolicy(perPolicyNoPolicies)).toBe(true); + }); + + it('should return `false` if artifact is global', () => { + expect(isArtifactByPolicy(globalEntry)).toBe(false); + }); + }); + + describe('when using `getPolicyIdsFromArtifact()`', () => { + it('should return array of policies', () => { + expect(getPolicyIdsFromArtifact(perPolicyWithPolicy)).toEqual(['123', '456']); + }); + + it('should return empty array if there are none', () => { + expect(getPolicyIdsFromArtifact(perPolicyNoPolicies)).toEqual([]); + }); + }); + + describe('when using `createExceptionListItemForCreate()`', () => { + it('should return an empty exception list ready for create', () => { + expect(createExceptionListItemForCreate('abc')).toEqual({ + comments: [], + description: '', + entries: [], + item_id: undefined, + list_id: 'abc', + meta: { + temporaryUuid: expect.any(String), + }, + name: '', + namespace_type: 'agnostic', + tags: [GLOBAL_ARTIFACT_TAG], + type: 'simple', + os_types: ['windows'], + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts index 4cc39e9fb89802..332667064a605d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/utils.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListItemSchema, + CreateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import uuid from 'uuid'; import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants'; const POLICY_ID_START_POSITION = BY_POLICY_ARTIFACT_TAG_PREFIX.length; @@ -30,3 +34,21 @@ export const getPolicyIdsFromArtifact = (item: Pick { + return { + comments: [], + description: '', + entries: [], + item_id: undefined, + list_id: listId, + meta: { + temporaryUuid: uuid.v4(), + }, + name: '', + namespace_type: 'agnostic', + tags: [GLOBAL_ARTIFACT_TAG], + type: 'simple', + os_types: ['windows'], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx index cd293aed95d602..464e966f363497 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx @@ -422,7 +422,13 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ }, [hasOsSelection, selectedOs]); return ( - +

{addExceptionMessage}

diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 14820c4b34315e..b5e630de50f79d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -29,7 +29,7 @@ import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_ import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/flyout'; import { useAlertsActions } from './use_alerts_actions'; -import { useExceptionModal } from './use_add_exception_modal'; +import { useExceptionFlyout } from './use_add_exception_flyout'; import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -125,12 +125,12 @@ const AlertContextMenuComponent: React.FC )} - {exceptionModalType != null && + {exceptionFlyoutType != null && ruleId != null && ruleName != null && ecsRowData?._id != null && ( @@ -212,7 +212,7 @@ const AlertContextMenuComponent: React.FC void; onAddExceptionCancel: () => void; onAddExceptionConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; ruleIndices: string[]; } -export const useExceptionModal = ({ +export const useExceptionFlyout = ({ ruleIndex, refetch, timelineId, -}: UseExceptionModalProps): UseExceptionModal => { - const [exceptionModalType, setOpenAddExceptionModal] = useState(null); +}: UseExceptionFlyoutProps): UseExceptionFlyout => { + const [exceptionFlyoutType, setOpenAddExceptionFlyout] = useState(null); const ruleIndices = useMemo((): string[] => { if (ruleIndex != null) { @@ -41,11 +41,11 @@ export const useExceptionModal = ({ }, [ruleIndex]); const onAddExceptionTypeClick = useCallback((exceptionListType: ExceptionListType): void => { - setOpenAddExceptionModal(exceptionListType); + setOpenAddExceptionFlyout(exceptionListType); }, []); const onAddExceptionCancel = useCallback(() => { - setOpenAddExceptionModal(null); + setOpenAddExceptionFlyout(null); }, []); const onAddExceptionConfirm = useCallback( @@ -53,13 +53,13 @@ export const useExceptionModal = ({ if (refetch && (timelineId !== TimelineId.active || didBulkCloseAlert)) { refetch(); } - setOpenAddExceptionModal(null); + setOpenAddExceptionFlyout(null); }, [refetch, timelineId] ); return { - exceptionModalType, + exceptionFlyoutType, onAddExceptionTypeClick, onAddExceptionCancel, onAddExceptionConfirm, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx index 955c3576736892..936dd283793cb1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { - AlertAction, + RuleAction, ActionTypeRegistryContract, } from '../../../../../../triggers_actions_ui/public'; import { @@ -23,7 +23,7 @@ import { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import { getActionTypeName, validateMustache, validateActionParams } from './utils'; export const validateSingleAction = async ( - actionItem: AlertAction, + actionItem: RuleAction, actionTypeRegistry: ActionTypeRegistryContract ): Promise => { const actionParamsErrors = await validateActionParams(actionItem, actionTypeRegistry); @@ -37,7 +37,7 @@ export const validateRuleActionsField = async ( ...data: Parameters ): Promise | void | undefined> => { - const [{ value, path }] = data as [{ value: AlertAction[]; path: string }]; + const [{ value, path }] = data as [{ value: RuleAction[]; path: string }]; const errors = []; for (const actionItem of value) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts index fc9562af835250..4826a10e978bdb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts @@ -9,12 +9,12 @@ import mustache from 'mustache'; import { uniq, startCase, flattenDeep, isArray, isString } from 'lodash/fp'; import { - AlertAction, + RuleAction, ActionTypeRegistryContract, } from '../../../../../../triggers_actions_ui/public'; import * as I18n from './translations'; -export const getActionTypeName = (actionTypeId: AlertAction['actionTypeId']) => { +export const getActionTypeName = (actionTypeId: RuleAction['actionTypeId']) => { if (!actionTypeId) return ''; const actionType = actionTypeId.split('.')[1]; @@ -23,7 +23,7 @@ export const getActionTypeName = (actionTypeId: AlertAction['actionTypeId']) => return startCase(actionType); }; -export const validateMustache = (params: AlertAction['params']) => { +export const validateMustache = (params: RuleAction['params']) => { const errors: string[] = []; Object.entries(params).forEach(([paramKey, paramValue]) => { if (!isString(paramValue)) return; @@ -38,7 +38,7 @@ export const validateMustache = (params: AlertAction['params']) => { }; export const validateActionParams = async ( - actionItem: AlertAction, + actionItem: RuleAction, actionTypeRegistry: ActionTypeRegistryContract ): Promise => { const actionErrors = await actionTypeRegistry diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index c3f3131e65519d..29df35290a4458 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -274,7 +274,7 @@ const RuleDetailsPageComponent: React.FC = ({ spacesApi.ui.redirectLegacyUrl( path, i18nTranslate.translate( - 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + 'xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun', { defaultMessage: 'rule', } @@ -295,7 +295,7 @@ const RuleDetailsPageComponent: React.FC = ({ {spacesApi.ui.components.getLegacyUrlConflict({ objectNoun: i18nTranslate.translate( - 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + 'xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun', { defaultMessage: 'rule', } diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts index 12da54a992becb..8b88fcaff8a780 100644 --- a/x-pack/plugins/security_solution/public/management/common/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.ts @@ -7,7 +7,10 @@ import { isEmpty } from 'lodash/fp'; -export const parseQueryFilterToKQL = (filter: string, fields: Readonly): string => { +export const parseQueryFilterToKQL = ( + filter: string | undefined, + fields: Readonly +): string => { if (!filter) return ''; const kuery = fields .map( @@ -66,7 +69,7 @@ export const parsePoliciesAndFilterToKql = ({ kuery?: string; }): string | undefined => { if (policies?.length === 0 && excludedPolicies?.length === 0) { - return kuery; + return kuery ? kuery : undefined; } const policiesKQL = parsePoliciesToKQL(policies, excludedPolicies); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts index 71a12308895598..d8e2eeb956c11f 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts @@ -11,3 +11,4 @@ export * from './artifact_entry_collapsible_card'; export * from './components/card_section_panel'; export * from './types'; export { CardCompressedHeaderLayout } from './components/card_compressed_header'; +export { useEndpointPoliciesToArtifactPolicies } from './hooks/use_endpoint_policies_to_artifact_policies'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx new file mode 100644 index 00000000000000..87673cf5c1e47d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -0,0 +1,290 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useMemo, useState } from 'react'; + +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; +import { useLocation } from 'react-router-dom'; +import { AdministrationListPage } from '../administration_list_page'; + +import { PaginatedContent, PaginatedContentProps } from '../paginated_content'; + +import { ArtifactEntryCard } from '../artifact_entry_card'; + +import { ArtifactListPageLabels, artifactListPageLabels } from './translations'; +import { useTestIdGenerator } from '../hooks/use_test_id_generator'; +import { ManagementPageLoader } from '../management_page_loader'; +import { SearchExceptions } from '../search_exceptions'; +import { + useArtifactCardPropsProvider, + UseArtifactCardPropsProviderProps, +} from './hooks/use_artifact_card_props_provider'; +import { NoDataEmptyState } from './components/no_data_empty_state'; +import { ArtifactFlyoutProps, MaybeArtifactFlyout } from './components/artifact_flyout'; +import { useIsFlyoutOpened } from './hooks/use_is_flyout_opened'; +import { useSetUrlParams } from './hooks/use_set_url_params'; +import { useWithArtifactListData } from './hooks/use_with_artifact_list_data'; +import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +import { ArtifactListPageUrlParams } from './types'; +import { useUrlParams } from './hooks/use_url_params'; +import { ListPageRouteState, MaybeImmutable } from '../../../../common/endpoint/types'; +import { DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS } from '../../../../common/endpoint/service/artifacts/constants'; +import { ArtifactDeleteModal } from './components/artifact_delete_modal'; +import { useGetEndpointSpecificPolicies } from '../../services/policies/hooks'; +import { getLoadPoliciesError } from '../../common/translations'; +import { useToasts } from '../../../common/lib/kibana'; +import { useMemoizedRouteState } from '../../common/hooks'; +import { BackToExternalAppSecondaryButton } from '../back_to_external_app_secondary_button'; +import { BackToExternalAppButton } from '../back_to_external_app_button'; + +type ArtifactEntryCardType = typeof ArtifactEntryCard; + +type ArtifactListPagePaginatedContentComponent = PaginatedContentProps< + ExceptionListItemSchema, + ArtifactEntryCardType +>; + +export interface ArtifactListPageProps { + apiClient: ExceptionsListApiClient; + /** The artifact Component that will be displayed in the Flyout for Create and Edit flows */ + ArtifactFormComponent: ArtifactFlyoutProps['FormComponent']; + /** A list of labels for the given artifact page. Not all have to be defined, only those that should override the defaults */ + labels: ArtifactListPageLabels; + /** A list of fields that will be used by the search functionality when a user enters a value in the searchbar */ + searchableFields?: MaybeImmutable; + flyoutSize?: EuiFlyoutSize; + 'data-test-subj'?: string; +} + +export const ArtifactListPage = memo( + ({ + apiClient, + ArtifactFormComponent, + searchableFields = DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS, + labels: _labels = {}, + 'data-test-subj': dataTestSubj, + }) => { + const { state: routeState } = useLocation(); + const getTestId = useTestIdGenerator(dataTestSubj); + const toasts = useToasts(); + const isFlyoutOpened = useIsFlyoutOpened(); + const setUrlParams = useSetUrlParams(); + const { + urlParams: { filter, includedPolicies }, + } = useUrlParams(); + + const { + isPageInitializing, + isFetching: isLoading, + data: listDataResponse, + uiPagination, + doesDataExist, + error, + refetch: refetchListData, + } = useWithArtifactListData(apiClient, searchableFields); + + const items = useMemo(() => { + return listDataResponse?.data ?? []; + }, [listDataResponse?.data]); + + const [selectedItemForDelete, setSelectedItemForDelete] = useState< + undefined | ExceptionListItemSchema + >(undefined); + + const [selectedItemForEdit, setSelectedItemForEdit] = useState< + undefined | ExceptionListItemSchema + >(undefined); + + const labels = useMemo(() => { + return { + ...artifactListPageLabels, + ..._labels, + }; + }, [_labels]); + + const handleOnCardActionClick = useCallback( + ({ type, item }) => { + switch (type) { + case 'edit': + setSelectedItemForEdit(item); + setUrlParams({ show: 'edit', itemId: item.item_id }); + break; + + case 'delete': + setSelectedItemForDelete(item); + break; + } + }, + [setUrlParams] + ); + + const handleCardProps = useArtifactCardPropsProvider({ + items, + onAction: handleOnCardActionClick, + cardActionDeleteLabel: labels.cardActionDeleteLabel, + cardActionEditLabel: labels.cardActionEditLabel, + dataTestSubj: getTestId('card'), + }); + + const policiesRequest = useGetEndpointSpecificPolicies({ + onError: (err) => { + toasts.addWarning(getLoadPoliciesError(err)); + }, + }); + + const memoizedRouteState = useMemoizedRouteState(routeState); + + const backButtonEmptyComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + + const backButtonHeaderComponent = useMemo(() => { + if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { + return ; + } + }, [memoizedRouteState]); + + const handleOpenCreateFlyoutClick = useCallback(() => { + setUrlParams({ show: 'create' }); + }, [setUrlParams]); + + const handlePaginationChange: ArtifactListPagePaginatedContentComponent['onChange'] = + useCallback( + ({ pageIndex, pageSize }) => { + setUrlParams({ + page: pageIndex + 1, + pageSize, + }); + + // Scroll to the top to ensure that when new set of data is received and list updated, + // the user is back at the top of the list + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + }, + [setUrlParams] + ); + + const handleOnSearch = useCallback( + (filterValue: string, selectedPolicies: string, doHardRefresh) => { + setUrlParams({ + // `undefined` will drop the param from the url + filter: filterValue.trim() === '' ? undefined : filterValue, + includedPolicies: selectedPolicies.trim() === '' ? undefined : selectedPolicies, + }); + + if (doHardRefresh) { + refetchListData(); + } + }, + [refetchListData, setUrlParams] + ); + + const handleArtifactDeleteModalOnSuccess = useCallback(() => { + setSelectedItemForDelete(undefined); + refetchListData(); + }, [refetchListData]); + + const handleArtifactDeleteModalOnCancel = useCallback(() => { + setSelectedItemForDelete(undefined); + }, []); + + const handleArtifactFlyoutOnSuccess = useCallback(() => { + refetchListData(); + }, [refetchListData]); + + if (isPageInitializing) { + return ; + } + + return ( + + {labels.pageAddButtonTitle} + + } + > + {/* Flyout component is driven by URL params and may or may not be displayed based on those */} + + + {selectedItemForDelete && ( + + )} + + {!doesDataExist ? ( + + ) : ( + <> + + + + + + {labels.getShowingCountLabel(uiPagination.totalItemCount)} + + + + + + items={items} + ItemComponent={ArtifactEntryCard} + itemComponentProps={handleCardProps} + onChange={handlePaginationChange} + error={error} + loading={isLoading} + pagination={uiPagination} + contentClassName="card-container" + data-test-subj={getTestId('cardContent')} + /> + + )} + + ); + } +); +ArtifactListPage.displayName = 'ArtifactListPage'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.tsx new file mode 100644 index 00000000000000..4228d923a9ab30 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.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, { memo, useCallback } from 'react'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiCallOut, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { AutoFocusButton } from '../../../../common/components/autofocus_button/autofocus_button'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; +import { + getPolicyIdsFromArtifact, + isArtifactGlobal, +} from '../../../../../common/endpoint/service/artifacts'; +import { + ARTIFACT_DELETE_ACTION_LABELS, + useArtifactDeleteItem, +} from '../hooks/use_artifact_delete_item'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; + +export const ARTIFACT_DELETE_LABELS = Object.freeze({ + deleteModalTitle: (itemName: string): string => + i18n.translate('xpack.securitySolution.artifactListPage.deleteModalTitle', { + defaultMessage: 'Delete {itemName}', + values: { itemName }, + }), + + deleteModalImpactTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.deleteModalImpactTitle', + { + defaultMessage: 'Warning', + } + ), + + deleteModalImpactInfo: (item: ExceptionListItemSchema): string => { + return i18n.translate('xpack.securitySolution.artifactListPage.deleteModalImpactInfo', { + defaultMessage: + 'Deleting this entry will remove it from {count} associated {count, plural, one {policy} other {policies}}.', + values: { + count: isArtifactGlobal(item) + ? i18n.translate('xpack.securitySolution.artifactListPage.deleteModalImpactInfoAll', { + defaultMessage: 'all', + }) + : getPolicyIdsFromArtifact(item).length, + }, + }); + }, + + deleteModalConfirmInfo: i18n.translate( + 'xpack.securitySolution.artifactListPage.deleteModalConfirmInfo', + { + defaultMessage: 'This action cannot be undone. Are you sure you wish to continue?', + } + ), + + deleteModalSubmitButtonTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.deleteModalSubmitButtonTitle', + { defaultMessage: 'Delete' } + ), + + deleteModalCancelButtonTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.deleteModalCancelButtonTitle', + { defaultMessage: 'Cancel' } + ), +}); + +interface DeleteArtifactModalProps { + apiClient: ExceptionsListApiClient; + item: ExceptionListItemSchema; + onCancel: () => void; + onSuccess: () => void; + labels: typeof ARTIFACT_DELETE_LABELS & typeof ARTIFACT_DELETE_ACTION_LABELS; + 'data-test-subj'?: string; +} + +export const ArtifactDeleteModal = memo( + ({ apiClient, item, onCancel, onSuccess, 'data-test-subj': dataTestSubj, labels }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + const { deleteArtifactItem, isLoading: isDeleting } = useArtifactDeleteItem(apiClient, labels); + + const onConfirm = useCallback(() => { + deleteArtifactItem(item).then(() => onSuccess()); + }, [deleteArtifactItem, item, onSuccess]); + + const handleOnCancel = useCallback(() => { + if (!isDeleting) { + onCancel(); + } + }, [isDeleting, onCancel]); + + return ( + + + {labels.deleteModalTitle(item.name)} + + + + + +

+ {labels.deleteModalImpactInfo(item)} +

+
+ +

{labels.deleteModalConfirmInfo}

+
+
+ + + + {labels.deleteModalCancelButtonTitle} + + + + {labels.deleteModalSubmitButtonTitle} + + +
+ ); + } +); +ArtifactDeleteModal.displayName = 'ArtifactDeleteModal'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx new file mode 100644 index 00000000000000..483695de738249 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; +import { useUrlParams } from '../hooks/use_url_params'; +import { useIsFlyoutOpened } from '../hooks/use_is_flyout_opened'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; +import { useSetUrlParams } from '../hooks/use_set_url_params'; +import { useArtifactGetItem } from '../hooks/use_artifact_get_item'; +import { + ArtifactFormComponentOnChangeCallbackProps, + ArtifactFormComponentProps, + ArtifactListPageUrlParams, +} from '../types'; +import { ManagementPageLoader } from '../../management_page_loader'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { useToasts } from '../../../../common/lib/kibana'; +import { createExceptionListItemForCreate } from '../../../../../common/endpoint/service/artifacts/utils'; +import { useWithArtifactSubmitData } from '../hooks/use_with_artifact_submit_data'; +import { useIsArtifactAllowedPerPolicyUsage } from '../hooks/use_is_artifact_allowed_per_policy_usage'; + +export const ARTIFACT_FLYOUT_LABELS = Object.freeze({ + flyoutEditTitle: i18n.translate('xpack.securitySolution.artifactListPage.flyoutEditTitle', { + defaultMessage: 'Add artifact', + }), + + flyoutCreateTitle: i18n.translate('xpack.securitySolution.artifactListPage.flyoutCreateTitle', { + defaultMessage: 'Create artifact', + }), + flyoutCancelButtonLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.flyoutCancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + flyoutCreateSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.flyoutCreateSubmitButtonLabel', + { defaultMessage: 'Add' } + ), + flyoutEditSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.flyoutEditSubmitButtonLabel', + { defaultMessage: 'Save' } + ), + flyoutDowngradedLicenseTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.expiredLicenseTitle', + { + defaultMessage: 'Expired License', + } + ), + flyoutDowngradedLicenseInfo: i18n.translate( + 'xpack.securitySolution.artifactListPage.flyoutDowngradedLicenseInfo', + { + defaultMessage: + 'Your Kibana license has been downgraded. Future policy configurations will now be globally assigned to all policies.', + } + ), + /** + * This should be set to a sentence that includes a link to the documentation page for this specific artifact type. + * + * @example + * // in a component + * () => { + * const { docLinks } = useKibana().services; + * return ( + * + * + * + * }} + * /> + * ); + * } + */ + flyoutDowngradedLicenseDocsInfo: (): React.ReactNode => + i18n.translate('xpack.securitySolution.artifactListPage.flyoutDowngradedLicenseDocsInfo', { + defaultMessage: 'For more information, see our documentation.', + }), + + flyoutEditItemLoadFailure: (errorMessage: string) => + i18n.translate('xpack.securitySolution.artifactListPage.flyoutEditItemLoadFailure', { + defaultMessage: 'Failed to retrieve item for edit. Reason: {errorMessage}', + values: { errorMessage }, + }), + + /** + * A function returning the label for the success message toast + * @param itemName + * @example + * ({ name }) => i18n.translate('xpack.securitySolution.some_page.flyoutCreateSubmitSuccess', { + * defaultMessage: '"{name}" has been added.', + * values: { name }, + * }) + */ + flyoutCreateSubmitSuccess: ({ name }: ExceptionListItemSchema) => + i18n.translate('xpack.securitySolution.some_page.flyoutCreateSubmitSuccess', { + defaultMessage: '"{name}" has been added to your event filters.', + values: { name }, + }), + + /** + * Returns the edit success message for the toast + * @param item + * @example + * ({ name }) => + * i18n.translate('xpack.securitySolution.some_page.flyoutEditSubmitSuccess', { + * defaultMessage: '"{name}" has been updated.', + * values: { name }, + * }) + */ + flyoutEditSubmitSuccess: ({ name }: ExceptionListItemSchema) => + i18n.translate('xpack.securitySolution.artifactListPage.flyoutEditSubmitSuccess', { + defaultMessage: '"{name}" has been updated.', + values: { name }, + }), +}); + +const createFormInitialState = ( + listId: string, + item: ArtifactFormComponentOnChangeCallbackProps['item'] | undefined +): ArtifactFormComponentOnChangeCallbackProps => { + return { + isValid: false, + item: item ?? createExceptionListItemForCreate(listId), + }; +}; + +export interface ArtifactFlyoutProps { + apiClient: ExceptionsListApiClient; + FormComponent: React.ComponentType; + onSuccess(): void; + /** + * If the artifact data is provided and it matches the id in the URL, then it will not be + * retrieved again via the API + */ + item?: ExceptionListItemSchema; + /** Any label overrides */ + labels?: Partial; + 'data-test-subj'?: string; + size?: EuiFlyoutSize; +} + +/** + * Show the flyout based on URL params + */ +export const MaybeArtifactFlyout = memo( + ({ + apiClient, + item, + FormComponent, + onSuccess, + labels: _labels = {}, + 'data-test-subj': dataTestSubj, + size = 'm', + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const toasts = useToasts(); + const isFlyoutOpened = useIsFlyoutOpened(); + const setUrlParams = useSetUrlParams(); + const { urlParams } = useUrlParams(); + const labels = useMemo(() => { + return { + ...ARTIFACT_FLYOUT_LABELS, + ..._labels, + }; + }, [_labels]); + + const isEditFlow = urlParams.show === 'edit'; + const formMode: ArtifactFormComponentProps['mode'] = isEditFlow ? 'edit' : 'create'; + + const { + isLoading: isSubmittingData, + mutateAsync: submitData, + error: submitError, + } = useWithArtifactSubmitData(apiClient, formMode); + + const { + isLoading: isLoadingItemForEdit, + error, + refetch: fetchItemForEdit, + } = useArtifactGetItem(apiClient, urlParams.itemId ?? '', false); + + const [formState, setFormState] = useState( + createFormInitialState.bind(null, apiClient.listId, item) + ); + const showExpiredLicenseBanner = useIsArtifactAllowedPerPolicyUsage( + { tags: formState.item.tags ?? [] }, + formMode + ); + + const hasItemDataForEdit = useMemo(() => { + // `item_id` will not be defined for a `create` flow, so we use it below to determine if we + // are still attempting to load the item for edit from the api + return !!item || !!formState.item.item_id; + }, [formState.item.item_id, item]); + + const isInitializing = useMemo(() => { + return isEditFlow && !hasItemDataForEdit; + }, [hasItemDataForEdit, isEditFlow]); + + const handleFlyoutClose = useCallback(() => { + if (isSubmittingData) { + return; + } + + // `undefined` will cause params to be dropped from url + setUrlParams({ id: undefined, show: undefined }, true); + }, [isSubmittingData, setUrlParams]); + + const handleFormComponentOnChange: ArtifactFormComponentProps['onChange'] = useCallback( + ({ item: updatedItem, isValid }) => { + setFormState({ + item: updatedItem, + isValid, + }); + }, + [] + ); + + const handleSubmitClick = useCallback(() => { + submitData(formState.item).then((result) => { + toasts.addSuccess( + isEditFlow + ? labels.flyoutEditSubmitSuccess(result) + : labels.flyoutCreateSubmitSuccess(result) + ); + + // Close the flyout + // `undefined` will cause params to be dropped from url + setUrlParams({ id: undefined, show: undefined }, true); + }); + }, [formState.item, isEditFlow, labels, setUrlParams, submitData, toasts]); + + // If we don't have the actual Artifact data yet for edit (in initialization phase - ex. came in with an + // ID in the url that was not in the list), then retrieve it now + useEffect(() => { + if (isEditFlow && !hasItemDataForEdit && !error && isInitializing && !isLoadingItemForEdit) { + fetchItemForEdit().then(({ data: editItemData }) => { + if (editItemData) { + setFormState(createFormInitialState(apiClient.listId, editItemData)); + } + }); + } + }, [ + apiClient.listId, + error, + fetchItemForEdit, + isEditFlow, + isInitializing, + isLoadingItemForEdit, + hasItemDataForEdit, + ]); + + // If we got an error while trying ot retrieve the item for edit, then show a toast message + useEffect(() => { + if (isEditFlow && error) { + toasts.addWarning(labels.flyoutEditItemLoadFailure(error?.body?.message || error.message)); + + // Blank out the url params for id and show (will close out the flyout) + setUrlParams({ id: undefined, show: undefined }); + } + }, [error, isEditFlow, labels, setUrlParams, toasts, urlParams.itemId]); + + if (!isFlyoutOpened || error) { + return null; + } + + return ( + + + +

{isEditFlow ? labels.flyoutEditTitle : labels.flyoutCreateTitle}

+
+
+ + {!isInitializing && showExpiredLicenseBanner && ( + + {`${labels.flyoutDowngradedLicenseInfo} ${labels.flyoutDowngradedLicenseDocsInfo()}`} + + )} + + + {isInitializing && } + + {!isInitializing && ( + + )} + + + {!isInitializing && ( + + + + + {labels.flyoutCancelButtonLabel} + + + + + {isEditFlow + ? labels.flyoutEditSubmitButtonLabel + : labels.flyoutCreateSubmitButtonLabel} + + + + + )} +
+ ); + } +); +MaybeArtifactFlyout.displayName = 'MaybeArtifactFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.tsx new file mode 100644 index 00000000000000..cbb6bd8c5454e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/no_data_empty_state.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, { memo } from 'react'; +import styled, { css } from 'styled-components'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { ManagementEmptyStateWrapper } from '../../management_empty_state_wrapper'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; + +const EmptyPrompt = styled(EuiEmptyPrompt)` + ${() => css` + max-width: 100%; + `} +`; + +export const NoDataEmptyState = memo<{ + onAdd: () => void; + titleLabel: string; + aboutInfo: string; + primaryButtonLabel: string; + /** Should the Add button be disabled */ + isAddDisabled?: boolean; + backComponent?: React.ReactNode; + 'data-test-subj'?: string; +}>( + ({ + onAdd, + isAddDisabled = false, + backComponent, + 'data-test-subj': dataTestSubj, + titleLabel, + aboutInfo, + primaryButtonLabel, + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + return ( + + {titleLabel}} + body={
{aboutInfo}
} + actions={[ + + {primaryButtonLabel} + , + ...(backComponent ? [backComponent] : []), + ]} + /> +
+ ); + } +); + +NoDataEmptyState.displayName = 'NoDataEmptyState'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_card_props_provider.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_card_props_provider.ts new file mode 100644 index 00000000000000..58a0f59feaa38b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_card_props_provider.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useCallback, useMemo } from 'react'; +import { + AnyArtifact, + ArtifactEntryCardProps, + useEndpointPoliciesToArtifactPolicies, +} from '../../artifact_entry_card'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; +import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; +import { getLoadPoliciesError } from '../../../common/translations'; +import { useToasts } from '../../../../common/lib/kibana'; + +type CardActionType = 'edit' | 'delete'; + +export interface UseArtifactCardPropsProviderProps { + items: ExceptionListItemSchema[]; + onAction: (action: { type: CardActionType; item: ExceptionListItemSchema }) => void; + cardActionEditLabel: string; + cardActionDeleteLabel: string; + dataTestSubj?: string; +} + +type ArtifactCardPropsProvider = (artifactItem: ExceptionListItemSchema) => ArtifactEntryCardProps; + +/** + * Return a function that can be used to retrieve props for an `ArtifactCardEntry` component given an + * `ExceptionListItemSchema` on input + */ +export const useArtifactCardPropsProvider = ({ + items, + onAction, + cardActionDeleteLabel, + cardActionEditLabel, + dataTestSubj, +}: UseArtifactCardPropsProviderProps): ArtifactCardPropsProvider => { + const getTestId = useTestIdGenerator(dataTestSubj); + const toasts = useToasts(); + + const { data: policyData } = useGetEndpointSpecificPolicies({ + onError: (error) => { + toasts.addDanger(getLoadPoliciesError(error)); + }, + }); + + const policies: ArtifactEntryCardProps['policies'] = useEndpointPoliciesToArtifactPolicies( + policyData?.items + ); + + const artifactCardPropsPerItem = useMemo(() => { + const cachedCardProps: Record = {}; + + // Casting `listItems` below to remove the `Immutable<>` from it in order to prevent errors + // with common component's props + for (const artifactItem of items as ExceptionListItemSchema[]) { + cachedCardProps[artifactItem.id] = { + item: artifactItem as AnyArtifact, + policies, + 'data-test-subj': dataTestSubj, + actions: [ + { + icon: 'controlsHorizontal', + onClick: () => { + onAction({ type: 'edit', item: artifactItem }); + }, + 'data-test-subj': getTestId('cardEditAction'), + children: cardActionEditLabel, + }, + { + icon: 'trash', + onClick: () => { + onAction({ type: 'delete', item: artifactItem }); + }, + 'data-test-subj': getTestId('cardDeleteAction'), + children: cardActionDeleteLabel, + }, + ], + hideDescription: !artifactItem.description, + hideComments: !artifactItem.comments.length, + }; + } + + return cachedCardProps; + }, [ + items, + policies, + dataTestSubj, + getTestId, + cardActionEditLabel, + cardActionDeleteLabel, + onAction, + ]); + + return useCallback( + (artifactItem: ExceptionListItemSchema) => { + return artifactCardPropsPerItem[artifactItem.id]; + }, + [artifactCardPropsPerItem] + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_create_item.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_create_item.ts new file mode 100644 index 00000000000000..4252d66f2a510d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_create_item.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { useMutation } from 'react-query'; +import { HttpFetchError } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; + +// FIXME: delete entire file once PR# 125198 is merged. This entire file was copied from that pr + +export interface CallbackTypes { + onSuccess?: (updatedException: ExceptionListItemSchema) => void; + onError?: (error?: HttpFetchError) => void; + onSettled?: () => void; +} + +export function useCreateArtifact( + exceptionListApiClient: ExceptionsListApiClient, + callbacks: CallbackTypes = {} +) { + const { onSuccess = () => {}, onError = () => {}, onSettled = () => {} } = callbacks; + + return useMutation< + ExceptionListItemSchema, + HttpFetchError, + CreateExceptionListItemSchema, + () => void + >( + async (exception: CreateExceptionListItemSchema) => { + return exceptionListApiClient.create(exception); + }, + { + onSuccess, + onError, + onSettled: () => { + onSettled(); + }, + } + ); +} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_delete_item.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_delete_item.ts new file mode 100644 index 00000000000000..feac0c2b0c5997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_delete_item.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 { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useMutation, UseMutationResult } from 'react-query'; +import { i18n } from '@kbn/i18n'; +import { useMemo } from 'react'; +import type { HttpFetchError } from 'kibana/public'; +import { useToasts } from '../../../../common/lib/kibana'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; + +export const ARTIFACT_DELETE_ACTION_LABELS = Object.freeze({ + /** + * Message to be displayed in toast when deletion fails + * @param itemName + * @param errorMessage + * @example + * (itemsName, errorMessage) => i18n.translate( + * 'xpack.securitySolution.artifactListPage.deleteActionFailure', + * { + * defaultMessage: 'Unable to remove "{itemName}" . Reason: {errorMessage}', + * values: { itemName, errorMessage }, + * }) + */ + deleteActionFailure: (itemName: string, errorMessage: string) => + i18n.translate('xpack.securitySolution.artifactListPage.deleteActionFailure', { + defaultMessage: 'Unable to remove "{itemName}" . Reason: {errorMessage}', + values: { itemName, errorMessage }, + }), + + /** + * Message to be displayed in the toast after a successful delete + * @param itemName + * @example + * (itemName) => i18n.translate('xpack.securitySolution.some_page.deleteSuccess', { + * defaultMessage: '"{itemName}" has been removed', + * values: { itemName }, + * }) + */ + deleteActionSuccess: (itemName: string) => + i18n.translate('xpack.securitySolution.artifactListPage.deleteActionSuccess', { + defaultMessage: '"{itemName}" has been removed', + values: { itemName }, + }), +}); + +type UseArtifactDeleteItemMutationResult = UseMutationResult< + ExceptionListItemSchema, + HttpFetchError, + ExceptionListItemSchema +>; + +export type UseArtifactDeleteItemInterface = UseArtifactDeleteItemMutationResult & { + deleteArtifactItem: UseArtifactDeleteItemMutationResult['mutateAsync']; +}; + +export const useArtifactDeleteItem = ( + apiClient: ExceptionsListApiClient, + labels: typeof ARTIFACT_DELETE_ACTION_LABELS +): UseArtifactDeleteItemInterface => { + const toasts = useToasts(); + + const mutation = useMutation( + async (item: ExceptionListItemSchema) => { + return apiClient.delete(item.item_id); + }, + { + onError: (error: HttpFetchError, item) => { + toasts.addDanger( + labels.deleteActionFailure(item.name, error.body?.message || error.message) + ); + }, + onSuccess: (response) => { + toasts.addSuccess(labels.deleteActionSuccess(response.name)); + }, + } + ); + + return useMemo(() => { + return { + ...mutation, + deleteArtifactItem: mutation.mutateAsync, + }; + }, [mutation]); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_get_item.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_get_item.ts new file mode 100644 index 00000000000000..21b13aa285376f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_get_item.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { HttpFetchError } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; + +export const useArtifactGetItem = ( + apiClient: ExceptionsListApiClient, + itemId: string, + enabled: boolean = true +) => { + return useQuery( + ['item', apiClient, itemId], + () => apiClient.get(itemId), + { + enabled, + refetchOnWindowFocus: false, + keepPreviousData: true, + retry: false, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_update_item.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_update_item.ts new file mode 100644 index 00000000000000..a217da0159ed87 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_artifact_update_item.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + UpdateExceptionListItemSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { useQueryClient, useMutation } from 'react-query'; +import { HttpFetchError } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; + +// FIXME: delete entire file once PR# 125198 is merged. This entire file was copied from that pr + +export interface CallbackTypes { + onSuccess?: (updatedException: ExceptionListItemSchema) => void; + onError?: (error?: HttpFetchError) => void; + onSettled?: () => void; +} + +export function useUpdateArtifact( + exceptionListApiClient: ExceptionsListApiClient, + callbacks: CallbackTypes = {} +) { + const queryClient = useQueryClient(); + const { onSuccess = () => {}, onError = () => {}, onSettled = () => {} } = callbacks; + + return useMutation< + ExceptionListItemSchema, + HttpFetchError, + UpdateExceptionListItemSchema, + () => void + >( + async (exception: UpdateExceptionListItemSchema) => { + return exceptionListApiClient.update(exception); + }, + { + onSuccess, + onError, + onSettled: () => { + queryClient.invalidateQueries(['list', exceptionListApiClient]); + queryClient.invalidateQueries(['get', exceptionListApiClient]); + onSettled(); + }, + } + ); +} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_artifact_allowed_per_policy_usage.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_artifact_allowed_per_policy_usage.ts new file mode 100644 index 00000000000000..08a51ca061fe0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_artifact_allowed_per_policy_usage.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 { useMemo } from 'react'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ArtifactFormComponentProps } from '../types'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { isArtifactByPolicy } from '../../../../../common/endpoint/service/artifacts'; + +export const useIsArtifactAllowedPerPolicyUsage = ( + item: Pick, + mode: ArtifactFormComponentProps['mode'] +): boolean => { + const endpointAuthz = useUserPrivileges().endpointPrivileges; + + return useMemo(() => { + return mode === 'edit' && !endpointAuthz.canCreateArtifactsByPolicy && isArtifactByPolicy(item); + }, [endpointAuthz.canCreateArtifactsByPolicy, item, mode]); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_flyout_opened.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_flyout_opened.ts new file mode 100644 index 00000000000000..dc53a58924e83d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_is_flyout_opened.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 { useMemo } from 'react'; +import { useUrlParams } from './use_url_params'; +import { ArtifactListPageUrlParams } from '../types'; + +const SHOW_VALUES: readonly string[] = ['create', 'edit']; + +export const useIsFlyoutOpened = (): boolean => { + const showUrlParamValue = useUrlParams().urlParams.show ?? ''; + return useMemo(() => SHOW_VALUES.includes(showUrlParamValue), [showUrlParamValue]); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_kuery_from_exceptions_search_filter.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_kuery_from_exceptions_search_filter.ts new file mode 100644 index 00000000000000..60923a26c694fa --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_kuery_from_exceptions_search_filter.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 { useMemo } from 'react'; +import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../../common/utils'; +import { MaybeImmutable } from '../../../../../common/endpoint/types'; + +export const useKueryFromExceptionsSearchFilter = ( + filter: string | undefined, + fields: MaybeImmutable, + policies: string | undefined +): string | undefined => { + return useMemo(() => { + return parsePoliciesAndFilterToKql({ + kuery: parseQueryFilterToKQL(filter, fields), + policies: policies ? policies.split(',') : [], + }); + }, [fields, filter, policies]); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_set_url_params.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_set_url_params.ts new file mode 100644 index 00000000000000..80ffdeb2539469 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_set_url_params.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 { useHistory, useLocation } from 'react-router-dom'; +import { useCallback } from 'react'; +import { pickBy } from 'lodash'; +import { useUrlParams } from './use_url_params'; + +// FIXME:PT delete/change once we get the common hook from @parkiino PR +export const useSetUrlParams = (): (( + /** Any param whose value is `undefined` will be removed from the URl when in append mode */ + params: Record, + replace?: boolean +) => void) => { + const location = useLocation(); + const history = useHistory(); + const { toUrlParams, urlParams: currentUrlParams } = useUrlParams(); + + return useCallback( + (params, replace = false) => { + history.push({ + ...location, + search: toUrlParams( + replace + ? params + : pickBy({ ...currentUrlParams, ...params }, (value) => value !== undefined) + ), + }); + }, + [currentUrlParams, history, location, toUrlParams] + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_url_params.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_url_params.ts new file mode 100644 index 00000000000000..7e1b8d16b37713 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_url_params.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 { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { parse, stringify } from 'query-string'; + +// FIXME:PT delete and use common hook once @parkiino merges +export function useUrlParams>(): { + urlParams: T; + toUrlParams: (params?: T) => string; +} { + const { search } = useLocation(); + return useMemo(() => { + const urlParams = parse(search) as unknown as T; + return { + urlParams, + toUrlParams: (params: T = urlParams) => stringify(params as unknown as object), + }; + }, [search]); +} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts new file mode 100644 index 00000000000000..3eca6c60bc711d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { QueryObserverResult } from 'react-query'; +import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useEffect, useMemo, useState } from 'react'; +import { Pagination } from '@elastic/eui'; +import { useQuery } from 'react-query'; +import type { ServerApiError } from '../../../../common/types'; +import { useIsMounted } from '../../hooks/use_is_mounted'; +import { + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_PAGE_SIZE_OPTIONS, +} from '../../../common/constants'; +import { useUrlParams } from './use_url_params'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { ArtifactListPageUrlParams } from '../types'; +import { MaybeImmutable } from '../../../../../common/endpoint/types'; +import { useKueryFromExceptionsSearchFilter } from './use_kuery_from_exceptions_search_filter'; + +type WithArtifactListDataInterface = QueryObserverResult< + FoundExceptionListItemSchema, + ServerApiError +> & { + /** + * Set to true during initialization of the page until it can be determined if either data exists. + * This should drive the showing of the overall page loading state if set to `true` + */ + isPageInitializing: boolean; + + /** + * Indicates if the exception list has any data at all (regardless of filters the user might have used) + */ + doesDataExist: boolean; + + /** + * The UI pagination data based on the data retrieved for the list + */ + uiPagination: Pagination; +}; + +export const useWithArtifactListData = ( + apiClient: ExceptionsListApiClient, + searchableFields: MaybeImmutable +): WithArtifactListDataInterface => { + const isMounted = useIsMounted(); + + const { + urlParams: { + page = 1, + pageSize = MANAGEMENT_DEFAULT_PAGE_SIZE, + sortOrder, + sortField, + filter, + includedPolicies, + }, + } = useUrlParams(); + + const kuery = useKueryFromExceptionsSearchFilter(filter, searchableFields, includedPolicies); + + const { + data: doesDataExist, + isFetching: isLoadingDataExists, + refetch: checkIfDataExists, + } = useQuery( + ['does-data-exists', apiClient], + async () => apiClient.hasData(), + { + enabled: true, + keepPreviousData: true, + refetchOnWindowFocus: false, + } + ); + + const [uiPagination, setUiPagination] = useState({ + totalItemCount: 0, + pageSize, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], + pageIndex: page - 1, + }); + + const [isPageInitializing, setIsPageInitializing] = useState(true); + + const listDataRequest = useQuery( + ['list', apiClient, page, pageSize, sortField, sortField, kuery], + async () => apiClient.find({ page, perPage: pageSize, filter: kuery, sortField, sortOrder }), + { + enabled: true, + keepPreviousData: true, + refetchOnWindowFocus: false, + } + ); + + const { + data: listData, + isFetching: isLoadingListData, + error: listDataError, + isSuccess: isSuccessListData, + } = listDataRequest; + + // Once we know if data exists, update the page initializing state. + // This should only ever happen at most once; + useEffect(() => { + if (isMounted) { + if (isPageInitializing === true && !isLoadingDataExists) { + setIsPageInitializing(false); + } + } + }, [isLoadingDataExists, isMounted, isPageInitializing]); + + // Update the uiPagination once the query succeeds + useEffect(() => { + if (isMounted && listData && !isLoadingListData && isSuccessListData) { + setUiPagination((prevState) => { + return { + ...prevState, + pageIndex: listData.page - 1, + pageSize: listData.per_page, + totalItemCount: listData.total, + }; + }); + } + }, [isLoadingListData, isMounted, isSuccessListData, listData]); + + // Keep the `doesDataExist` updated if we detect that list data result total is zero. + // Anytime: + // 1. the list data total is 0 + // 2. and page is 1 + // 3. and filter is empty + // 4. and doesDataExists is currently set to true + // check if data exists again + useEffect(() => { + if ( + isMounted && + !isLoadingListData && + !listDataError && + listData && + listData.total === 0 && + String(page) === '1' && + !kuery && + doesDataExist + ) { + checkIfDataExists(); + } + }, [ + checkIfDataExists, + doesDataExist, + filter, + includedPolicies, + isLoadingListData, + isMounted, + kuery, + listData, + listDataError, + page, + ]); + + return useMemo( + () => ({ + isPageInitializing, + doesDataExist: doesDataExist ?? false, + uiPagination, + ...listDataRequest, + }), + [doesDataExist, isPageInitializing, listDataRequest, uiPagination] + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_submit_data.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_submit_data.ts new file mode 100644 index 00000000000000..59a2739c9d3afe --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_submit_data.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 { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { ArtifactFormComponentProps } from '../types'; +import { useUpdateArtifact } from './use_artifact_update_item'; +import { useCreateArtifact } from './use_artifact_create_item'; + +export const useWithArtifactSubmitData = ( + apiClient: ExceptionsListApiClient, + mode: ArtifactFormComponentProps['mode'] +) => { + const artifactUpdater = useUpdateArtifact(apiClient); + const artifactCreator = useCreateArtifact(apiClient); + + return mode === 'create' ? artifactCreator : artifactUpdater; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/index.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/index.ts new file mode 100644 index 00000000000000..ba26a44259021f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/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 { ArtifactListPage } from './artifact_list_page'; +export type { ArtifactListPageProps } from './artifact_list_page'; +export * from './types'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/translations.ts new file mode 100644 index 00000000000000..ba6acf8a359aab --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/translations.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 { i18n } from '@kbn/i18n'; +import { ARTIFACT_FLYOUT_LABELS } from './components/artifact_flyout'; +import { ARTIFACT_DELETE_LABELS } from './components/artifact_delete_modal'; +import { ARTIFACT_DELETE_ACTION_LABELS } from './hooks/use_artifact_delete_item'; + +export const artifactListPageLabels = Object.freeze({ + // ------------------------------ + // PAGE labels + // ------------------------------ + pageTitle: i18n.translate('xpack.securitySolution.artifactListPage.pageTitle', { + defaultMessage: 'Artifact', + }), + pageAboutInfo: i18n.translate('xpack.securitySolution.artifactListPage.aboutInfo', { + defaultMessage: 'A list of artifacts for endpoint', + }), + pageAddButtonTitle: i18n.translate('xpack.securitySolution.artifactListPage.addButtonTitle', { + defaultMessage: 'Add artifact', + }), + + // ------------------------------ + // EMPTY state labels + // ------------------------------ + emptyStateTitle: i18n.translate('xpack.securitySolution.artifactListPage.emptyStateTitle', { + defaultMessage: 'Add your first artifact', + }), + emptyStateInfo: i18n.translate('xpack.securitySolution.artifactListPage.emptyStateInfo', { + defaultMessage: 'Add an artifact', + }), + emptyStatePrimaryButtonLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.emptyStatePrimaryButtonLabel', + { defaultMessage: 'Add' } + ), + + // ------------------------------ + // SEARCH BAR labels + // ------------------------------ + searchPlaceholderInfo: i18n.translate( + 'xpack.securitySolution.artifactListPage.searchPlaceholderInfo', + { + defaultMessage: 'Search on the fields below: name, description, comments, value', + } + ), + /** + * Return the label to show under the search bar with the total number of items that match the current filter (or all) + * @param total + * + * @example: + * (total) => i18n.translate('xpack.securitySolution.somepage.showingTotal', { + * defaultMessage: 'Showing {total} {total, plural, one {event filter} other {event filters}}', + * values: { total }, + * }) + */ + getShowingCountLabel: (total: number) => { + return i18n.translate('xpack.securitySolution.artifactListPage.showingTotal', { + defaultMessage: 'Showing {total, plural, one {# artifact} other {# artifacts}}', + values: { total }, + }); + }, + + // ------------------------------ + // CARD ACTIONS labels + // ------------------------------ + cardActionEditLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.cardActionEditLabel', + { + defaultMessage: 'Edit artifact', + } + ), + cardActionDeleteLabel: i18n.translate( + 'xpack.securitySolution.artifactListPage.cardActionDeleteLabel', + { + defaultMessage: 'Delete event filter', + } + ), + + // ------------------------------ + // ARTIFACT FLYOUT + // ------------------------------ + ...ARTIFACT_FLYOUT_LABELS, + + // ------------------------------ + // ARTIFACT DELETE MODAL + // ------------------------------ + ...ARTIFACT_DELETE_LABELS, + ...ARTIFACT_DELETE_ACTION_LABELS, +}); + +type IAllLabels = typeof artifactListPageLabels; + +/** + * The set of labels that normally have the artifact specific name in it, thus must be set for every page + */ +export type ArtifactListPageRequiredLabels = Pick< + IAllLabels, + | 'pageTitle' + | 'pageAboutInfo' + | 'pageAddButtonTitle' + | 'getShowingCountLabel' + | 'cardActionEditLabel' + | 'cardActionDeleteLabel' + | 'flyoutCreateTitle' + | 'flyoutEditTitle' + | 'flyoutCreateSubmitButtonLabel' + | 'flyoutCreateSubmitSuccess' + | 'flyoutEditSubmitSuccess' + | 'flyoutDowngradedLicenseDocsInfo' + | 'deleteActionSuccess' + | 'emptyStateTitle' + | 'emptyStateInfo' + | 'emptyStatePrimaryButtonLabel' +>; + +export type ArtifactListPageOptionalLabels = Omit; + +export type ArtifactListPageLabels = ArtifactListPageRequiredLabels & + Partial; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts new file mode 100644 index 00000000000000..fa63ebb863ce5c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.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 { HttpFetchError } from 'kibana/public'; +import type { + ExceptionListItemSchema, + CreateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; + +export interface ArtifactListPageUrlParams { + page?: number; + pageSize?: number; + filter?: string; + includedPolicies?: string; + show?: 'create' | 'edit'; + itemId?: string; + sortField?: string; + sortOrder?: string; +} + +export interface ArtifactFormComponentProps { + item: ExceptionListItemSchema | CreateExceptionListItemSchema; + mode: 'edit' | 'create'; + /** signals that the form should be made disabled (ex. while an update/create api call is in flight) */ + disabled: boolean; + /** Error will be set if the submission of the form to the api results in an API error. Form can use it to provide feedback to the user */ + error: HttpFetchError | undefined; + + /** reports the state of the form data and the current updated item */ + onChange(formStatus: ArtifactFormComponentOnChangeCallbackProps): void; +} + +export interface ArtifactFormComponentOnChangeCallbackProps { + isValid: boolean; + item: ExceptionListItemSchema | CreateExceptionListItemSchema; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_policy_link.tsx similarity index 74% rename from x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx rename to x-pack/plugins/security_solution/public/management/components/endpoint_policy_link.tsx index 2919fdd15e29d7..0417a65b570582 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_policy_link.tsx @@ -7,11 +7,9 @@ import React, { memo, useMemo } from 'react'; import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; -import { useEndpointSelector } from '../hooks'; -import { nonExistingPolicies } from '../../store/selectors'; -import { getPolicyDetailPath } from '../../../../common/routing'; -import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; +import { getPolicyDetailPath } from '../common/routing'; +import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { useAppUrl } from '../../common/lib/kibana/hooks'; /** * A policy link (to details) that first checks to see if the policy id exists against @@ -21,9 +19,9 @@ import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; export const EndpointPolicyLink = memo< Omit & { policyId: string; + missingPolicies?: Record; } ->(({ policyId, children, onClick, ...otherProps }) => { - const missingPolicies = useEndpointSelector(nonExistingPolicies); +>(({ policyId, children, onClick, missingPolicies = {}, ...otherProps }) => { const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { const path = getPolicyDetailPath(policyId); diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_is_mounted.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_is_mounted.ts new file mode 100644 index 00000000000000..c3ab4472cf4294 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/hooks/use_is_mounted.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 { useEffect, useRef } from 'react'; + +/** + * Track when a comonent is mounted/unmounted. Good for use in async processing that may update + * a component's internal state. + */ +export const useIsMounted = (): boolean => { + const isMounted = useRef(false); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return isMounted.current; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_url_pagination.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_url_pagination.ts new file mode 100644 index 00000000000000..84524ec2b7a48b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/hooks/use_url_pagination.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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { MANAGEMENT_DEFAULT_PAGE_SIZE, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../common/constants'; +import { useUrlParams } from './use_url_params'; + +// Page is not index-based, it is 1-based +interface Pagination { + page: number; + pageSize: number; +} + +type SetUrlPagination = (pagination: Pagination) => void; +interface UrlPagination { + pagination: Pagination; + setPagination: SetUrlPagination; + pageSizeOptions: number[]; +} + +type UrlPaginationParams = Partial; + +const paginationFromUrlParams = (urlParams: UrlPaginationParams): Pagination => { + const pagination: Pagination = { + pageSize: MANAGEMENT_DEFAULT_PAGE_SIZE, + page: 1, + }; + + // Search params can appear multiple times in the URL, in which case the value for them, + // once parsed, would be an array. In these case, we take the last value defined + pagination.page = Number( + (Array.isArray(urlParams.page) ? urlParams.page[urlParams.page.length - 1] : urlParams.page) ?? + pagination.page + ); + pagination.pageSize = + Number( + (Array.isArray(urlParams.pageSize) + ? urlParams.pageSize[urlParams.pageSize.length - 1] + : urlParams.pageSize) ?? pagination.pageSize + ) ?? pagination.pageSize; + + // If Current Page is not a valid positive integer, set it to 1 + if (!Number.isFinite(pagination.page) || pagination.page < 1) { + pagination.page = 1; + } + + // if pageSize is not one of the expected page sizes, reset it to 10 (default) + if (!MANAGEMENT_PAGE_SIZE_OPTIONS.includes(pagination.pageSize)) { + pagination.pageSize = MANAGEMENT_DEFAULT_PAGE_SIZE; + } + + return pagination; +}; + +/** + * Uses URL params for pagination and also persists those to the URL as they are updated + */ +export const useUrlPagination = (): UrlPagination => { + const location = useLocation(); + const history = useHistory(); + const { urlParams, toUrlParams } = useUrlParams(); + const urlPaginationParams = useMemo(() => { + return paginationFromUrlParams(urlParams); + }, [urlParams]); + const [pagination, setPagination] = useState(urlPaginationParams); + const setUrlPagination = useCallback( + ({ pageSize, page }) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + page, + pageSize, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + useEffect(() => { + setPagination((prevState) => { + return { + ...prevState, + ...paginationFromUrlParams(urlParams), + }; + }); + }, [setPagination, urlParams]); + + return { + pagination, + setPagination: setUrlPagination, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_url_params.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_url_params.ts new file mode 100644 index 00000000000000..d5dc2a00058869 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/hooks/use_url_params.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { parse, stringify, ParsedQuery } from 'query-string'; +import { useLocation } from 'react-router-dom'; + +/** + * Parses `search` params and returns an object with them along with a `toUrlParams` function + * that allows being able to retrieve a stringified version of an object (default is the + * `urlParams` that was parsed) for use in the url. + * Object will be recreated every time `search` changes. + */ +export function useUrlParams(): { + urlParams: ParsedQuery; + toUrlParams: (params: ParsedQuery) => string; +} { + const { search } = useLocation(); + return useMemo(() => { + const urlParams = parse(search); + return { + urlParams, + toUrlParams: (params = urlParams) => stringify(params), + }; + }, [search]); +} diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx index cc1104127871fa..20941c11b1593a 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_loader.tsx @@ -9,7 +9,7 @@ import React, { memo } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { ManagementEmptyStateWrapper } from './management_empty_state_wrapper'; -export const ManagementPageLoader = memo<{ 'data-test-subj': string }>( +export const ManagementPageLoader = memo<{ 'data-test-subj'?: string }>( ({ 'data-test-subj': dataTestSubj }) => { return ( diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index b6a15c04b3b064..6e86c69c497509 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -83,7 +83,7 @@ describe('Search exceptions', () => { }); expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, ''); + expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', false); }); it('should dispatch search action when click on button', () => { @@ -96,7 +96,7 @@ describe('Search exceptions', () => { }); expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, ''); + expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', true); }); it('should hide refresh button', () => { diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 52eb8d1e7d4f11..7a7a28b4b0647b 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -19,7 +19,14 @@ export interface SearchExceptionsProps { policyList?: ImmutableArray; defaultIncludedPolicies?: string; hideRefreshButton?: boolean; - onSearch(query: string, includedPolicies?: string): void; + onSearch( + /** The query string the user entered into the text field */ + query: string, + /** A list of policy id's comma delimited */ + includedPolicies: string | undefined, + /** Will be `true` if the user clicked the `refresh` button */ + refresh: boolean + ): void; } export const SearchExceptions = memo( @@ -45,7 +52,7 @@ export const SearchExceptions = memo( setIncludedPolicies(includePoliciesNew); - onSearch(query, includePoliciesNew); + onSearch(query, includePoliciesNew, false); }, [onSearch, query] ); @@ -55,13 +62,13 @@ export const SearchExceptions = memo( [setQuery] ); const handleOnSearch = useCallback( - () => onSearch(query, includedPolicies), + () => onSearch(query, includedPolicies, true), [onSearch, query, includedPolicies] ); const handleOnSearchQuery = useCallback( (value) => { - onSearch(value, includedPolicies); + onSearch(value, includedPolicies, false); }, [onSearch, includedPolicies] ); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx index a7bae8c1f37d60..152b2c3f0d6906 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx @@ -51,19 +51,21 @@ describe('Bulk delete artifact hook', () => { expect(fakeHttpServices.delete).toHaveBeenCalledTimes(0); await act(async () => { - const res = await result.mutateAsync(['fakeId-1', 'fakeId-2']); + const res = await result.mutateAsync([{ id: 'fakeId-1' }, { itemId: 'fakeId-2' }]); expect(res).toEqual([exceptionItem1, exceptionItem2]); expect(onSuccessMock).toHaveBeenCalledTimes(1); expect(fakeHttpServices.delete).toHaveBeenCalledTimes(2); expect(fakeHttpServices.delete).toHaveBeenNthCalledWith(1, '/api/exception_lists/items', { query: { id: 'fakeId-1', + item_id: undefined, namespace_type: 'agnostic', }, }); expect(fakeHttpServices.delete).toHaveBeenNthCalledWith(2, '/api/exception_lists/items', { query: { - id: 'fakeId-2', + id: undefined, + item_id: 'fakeId-2', namespace_type: 'agnostic', }, }); @@ -90,7 +92,7 @@ describe('Bulk delete artifact hook', () => { await act(async () => { try { - await result.mutateAsync(['fakeId-1', 'fakeId-2']); + await result.mutateAsync([{ id: 'fakeId-1' }, { id: 'fakeId-2' }]); } catch (err) { expect(err).toBe(error); expect(fakeHttpServices.delete).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.tsx index 5feda65996ea1b..9957ff27d4bf93 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.tsx @@ -10,25 +10,34 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types' import { useMutation, UseMutationResult, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useBulkDeleteArtifact( exceptionListApiClient: ExceptionsListApiClient, - customOptions: UseQueryOptions, + customOptions: UseQueryOptions = DEFAULT_OPTIONS, options: { concurrency: number; } = { concurrency: 5, } -): UseMutationResult void> { - return useMutation void>( - (exceptionIds: string[]) => { - return pMap( - exceptionIds, - (id) => { - return exceptionListApiClient.delete(id); - }, - options - ); - }, - customOptions - ); +): UseMutationResult< + ExceptionListItemSchema[], + HttpFetchError, + Array<{ itemId?: string; id?: string }>, + () => void +> { + return useMutation< + ExceptionListItemSchema[], + HttpFetchError, + Array<{ itemId?: string; id?: string }>, + () => void + >((exceptionIds: Array<{ itemId?: string; id?: string }>) => { + return pMap( + exceptionIds, + ({ itemId, id }) => { + return exceptionListApiClient.delete(itemId, id); + }, + options + ); + }, customOptions); } diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.tsx index 181fe6dc9d7d5c..b4854209fa07be 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.tsx @@ -13,9 +13,11 @@ import { import { useMutation, UseMutationResult, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useBulkUpdateArtifact( exceptionListApiClient: ExceptionsListApiClient, - customOptions: UseQueryOptions, + customOptions: UseQueryOptions = DEFAULT_OPTIONS, options: { concurrency: number; } = { diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.tsx index 346c4f7b42a87e..74aa6752f371cc 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.tsx @@ -12,9 +12,11 @@ import { HttpFetchError } from 'kibana/public'; import { useMutation, UseMutationResult, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useCreateArtifact( exceptionListApiClient: ExceptionsListApiClient, - customOptions: UseQueryOptions + customOptions: UseQueryOptions = DEFAULT_OPTIONS ): UseMutationResult< ExceptionListItemSchema, HttpFetchError, diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx index 8d31ce8f059bb5..bed3faf237b9f3 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx @@ -49,7 +49,7 @@ describe('Delete artifact hook', () => { expect(fakeHttpServices.delete).toHaveBeenCalledTimes(0); await act(async () => { - const res = await result.mutateAsync('fakeId'); + const res = await result.mutateAsync({ id: 'fakeId' }); expect(res).toBe(exceptionItem); expect(onSuccessMock).toHaveBeenCalledTimes(1); expect(fakeHttpServices.delete).toHaveBeenCalledTimes(1); @@ -82,7 +82,7 @@ describe('Delete artifact hook', () => { await act(async () => { try { - await result.mutateAsync('fakeId'); + await result.mutateAsync({ itemId: 'fakeId' }); } catch (err) { expect(err).toBe(error); expect(fakeHttpServices.delete).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.tsx index 27820c73b740ae..e072eecb061307 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.tsx @@ -9,11 +9,23 @@ import { HttpFetchError } from 'kibana/public'; import { useMutation, UseMutationResult, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useDeleteArtifact( exceptionListApiClient: ExceptionsListApiClient, - customOptions: UseQueryOptions -): UseMutationResult void> { - return useMutation void>((id: string) => { - return exceptionListApiClient.delete(id); + customOptions: UseQueryOptions = DEFAULT_OPTIONS +): UseMutationResult< + ExceptionListItemSchema, + HttpFetchError, + { itemId?: string; id?: string }, + () => void +> { + return useMutation< + ExceptionListItemSchema, + HttpFetchError, + { itemId?: string; id?: string }, + () => void + >(({ itemId, id }) => { + return exceptionListApiClient.delete(itemId, id); }, customOptions); } diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.test.tsx index 70f9620c6edc99..470f29b563f675 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.test.tsx @@ -39,7 +39,7 @@ describe('Get artifact hook', () => { result = await renderQuery( () => - useGetArtifact(instance, 'fakeId', { + useGetArtifact(instance, 'fakeId', undefined, { onSuccess: onSuccessMock, retry: false, }), @@ -50,7 +50,7 @@ describe('Get artifact hook', () => { expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); expect(fakeHttpServices.get).toHaveBeenCalledWith('/api/exception_lists/items', { query: { - id: 'fakeId', + item_id: 'fakeId', namespace_type: 'agnostic', }, }); @@ -69,7 +69,7 @@ describe('Get artifact hook', () => { result = await renderQuery( () => - useGetArtifact(instance, 'fakeId', { + useGetArtifact(instance, undefined, 'fakeId', { onError: onErrorMock, retry: false, }), diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.tsx index bc27ba285ccb38..676c424e1a2789 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_artifact.tsx @@ -9,15 +9,18 @@ import { HttpFetchError } from 'kibana/public'; import { QueryObserverResult, useQuery, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useGetArtifact( exceptionListApiClient: ExceptionsListApiClient, - id: string, - customQueryOptions: UseQueryOptions + itemId?: string, + id?: string, + customQueryOptions: UseQueryOptions = DEFAULT_OPTIONS ): QueryObserverResult { return useQuery( - ['get', exceptionListApiClient, id], + ['get', exceptionListApiClient, itemId, id], () => { - return exceptionListApiClient.get(id); + return exceptionListApiClient.get(itemId, id); }, { refetchIntervalInBackground: false, diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.tsx index a972096bb600ce..54acea6489574b 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.tsx @@ -12,9 +12,11 @@ import { HttpFetchError } from 'kibana/public'; import { useMutation, UseMutationResult, UseQueryOptions } from 'react-query'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useUpdateArtifact( exceptionListApiClient: ExceptionsListApiClient, - customQueryOptions: UseQueryOptions + customQueryOptions: UseQueryOptions = DEFAULT_OPTIONS ): UseMutationResult< ExceptionListItemSchema, HttpFetchError, diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.test.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.test.tsx index 37f4004ba91744..2dab6a8fd497a8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { act } from '@testing-library/react'; +import { act, waitFor } from '@testing-library/react'; import React from 'react'; import { BLOCKLIST_PATH } from '../../../../../common/constants'; -import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { Blocklist } from './blocklist'; @@ -28,12 +27,12 @@ describe('When on the blocklist page', () => { }); }); - describe('When on the blocklist list page', () => { - describe('And no data exists', () => { - it('should show the Empty message', async () => { - render(); - expect(renderResult.getByTestId('blocklistEmpty')).toBeTruthy(); - }); + describe('And no data exists', () => { + it('should show the Empty message', async () => { + render(); + await waitFor(() => + expect(renderResult.getByTestId('blocklistPage-emptyState')).toBeTruthy() + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index ab96451f0cd25a..a48d6c5bd83774 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -5,80 +5,130 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButton } from '@elastic/eui'; -import { AdministrationListPage } from '../../../components/administration_list_page'; -import { ListPageRouteState } from '../../../../../common/endpoint/types'; -import { useMemoizedRouteState } from '../../../common/hooks'; -import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; -import { BlocklistEmptyState } from './components/empty'; -import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useHttp } from '../../../../common/lib/kibana'; +import { ArtifactListPage, ArtifactListPageProps } from '../../../components/artifact_list_page'; +import { HostIsolationExceptionsApiClient } from '../../host_isolation_exceptions/host_isolation_exceptions_api_client'; -export const Blocklist = memo(() => { - const { state: routeState } = useLocation(); - const memoizedRouteState = useMemoizedRouteState(routeState); +// FIXME:PT delete this when real component is implemented +const TempDevFormComponent: ArtifactListPageProps['ArtifactFormComponent'] = (props) => { + // For Dev. Delete once we implement this component + // @ts-ignore + if (!window._dev_artifact_form_props) { + // @ts-ignore + window._dev_artifact_form_props = []; + // @ts-ignore + window.console.log(window._dev_artifact_form_props); + } + // @ts-ignore + window._dev_artifact_form_props.push(props); - const backButtonEmptyComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); + return ( +
+
+ {props.error ? props.error?.body?.message || props.error : ''} +
+ {`TODO: ${props.mode} Form here`} +
+ ); +}; - const backButtonHeaderComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); +const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { + pageTitle: i18n.translate('xpack.securitySolution.blocklist.pageTitle', { + defaultMessage: 'Blocklist', + }), + pageAboutInfo: i18n.translate('xpack.securitySolution.blocklist.pageAboutInfo', { + defaultMessage: '(DEV: temporarily using isolation exception api)', // FIXME: need wording from PM + }), + pageAddButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageAddButtonTitle', { + defaultMessage: 'Add blocklist entry', + }), + getShowingCountLabel: (total) => + i18n.translate('xpack.securitySolution.blocklist.showingTotal', { + defaultMessage: 'Showing {total} {total, plural, one {blocklist} other {blocklists}}', + values: { total }, + }), + cardActionEditLabel: i18n.translate('xpack.securitySolution.blocklist.cardActionEditLabel', { + defaultMessage: 'Edit blocklist', + }), + cardActionDeleteLabel: i18n.translate('xpack.securitySolution.blocklist.cardActionDeleteLabel', { + defaultMessage: 'Delete blocklist', + }), + flyoutCreateTitle: i18n.translate('xpack.securitySolution.blocklist.flyoutCreateTitle', { + defaultMessage: 'Add blocklist', + }), + flyoutEditTitle: i18n.translate('xpack.securitySolution.blocklist.flyoutEditTitle', { + defaultMessage: 'Edit blocklist', + }), + flyoutCreateSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.blocklist.flyoutCreateSubmitButtonLabel', + { defaultMessage: 'Add blocklist' } + ), + flyoutCreateSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.blocklist.flyoutCreateSubmitSuccess', { + defaultMessage: '"{name}" has been added to your blocklist.', // FIXME: match this to design (needs count of items) + values: { name }, + }), + flyoutEditSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.blocklist.flyoutEditSubmitSuccess', { + defaultMessage: '"{name}" has been updated.', + values: { name }, + }), + flyoutDowngradedLicenseDocsInfo: () => { + return 'tbd...'; + // FIXME: define docs link for license downgrade message. sample code below - const hasDataToShow = false; + // const { docLinks } = useKibana().services; + // return ( + // + // {' '} + // {' '} + // + // ), + // }} + // /> + // ); + }, + deleteActionSuccess: (itemName) => + i18n.translate('xpack.securitySolution.blocklist.deleteSuccess', { + defaultMessage: '"{itemName}" has been removed from blocklist.', + values: { itemName }, + }), + emptyStateTitle: i18n.translate('xpack.securitySolution.blocklist.emptyStateTitle', { + defaultMessage: 'Add your first blocklist', + }), + emptyStateInfo: i18n.translate( + 'xpack.securitySolution.blocklist.emptyStateInfo', + { defaultMessage: 'Add a blocklist to prevent execution on the endpoint' } // FIXME: need wording here form PM + ), + emptyStatePrimaryButtonLabel: i18n.translate( + 'xpack.securitySolution.blocklist.emptyStatePrimaryButtonLabel', + { defaultMessage: 'Add blocklist' } + ), +}; - const handleAddButtonClick = () => {}; +export const Blocklist = memo(() => { + const http = useHttp(); + // FIXME: Implement Blocklist API client and define list + // for now, just using Event Filters + const eventFiltersApiClient = HostIsolationExceptionsApiClient.getInstance(http); return ( - - } - subtitle={ - - } - actions={ - hasDataToShow ? ( - - - - ) : ( - [] - ) - } - hideHeader={!hasDataToShow} - > - {hasDataToShow ? ( -

{'Data, search bar, etc here'}

- ) : ( - - )} -
+ ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/empty.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/empty.tsx deleted file mode 100644 index bd1d01b73ec8da..00000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/empty.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import styled, { css } from 'styled-components'; -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ManagementEmptyStateWrapper } from '../../../../components/management_empty_state_wrapper'; - -const EmptyPrompt = styled(EuiEmptyPrompt)` - ${() => css` - max-width: 100%; - `} -`; - -export const BlocklistEmptyState = memo<{ - onAdd: () => void; - backComponent?: React.ReactNode; -}>(({ onAdd, backComponent }) => { - return ( - - - - - } - body={ - - } - actions={[ - - - , - - ...(backComponent ? [backComponent] : []), - ]} - /> - - ); -}); - -BlocklistEmptyState.displayName = 'BlocklistEmptyState'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index 5f51436daff6a7..6dcedd8f905a9b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -20,12 +20,12 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { isPolicyOutOfDate } from '../../utils'; import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types'; import { useEndpointSelector } from '../hooks'; -import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; +import { nonExistingPolicies, policyResponseStatus, uiQueryParams } from '../../store/selectors'; import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { getEndpointDetailsPath } from '../../../../common/routing'; -import { EndpointPolicyLink } from '../components/endpoint_policy_link'; +import { EndpointPolicyLink } from '../../../../components/endpoint_policy_link'; import { OutOfDate } from '../components/out_of_date'; import { EndpointAgentStatus } from '../components/endpoint_agent_status'; @@ -64,6 +64,8 @@ export const EndpointDetailsContent = memo( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; + const missingPolicies = useEndpointSelector(nonExistingPolicies); + const policyResponseRoutePath = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention const { selected_endpoint, show, ...currentUrlParams } = queryParams; @@ -131,6 +133,7 @@ export const EndpointDetailsContent = memo( policyId={details.Endpoint.policy.applied.id} data-test-subj="policyDetailsValue" className={'policyLineText'} + missingPolicies={missingPolicies} > {details.Endpoint.policy.applied.name} @@ -211,7 +214,7 @@ export const EndpointDetailsContent = memo( ), }, ]; - }, [details, hostStatus, policyStatus, policyStatusClickHandler, policyInfo]); + }, [details, hostStatus, policyStatus, policyStatusClickHandler, policyInfo, missingPolicies]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 3ac3eda0426f7b..46b9fef4df3578 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -39,6 +39,7 @@ import { DEFAULT_POLL_INTERVAL, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../co import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { EndpointPolicyLink } from '../../../components/endpoint_policy_link'; import { CreatePackagePolicyRouteState, AgentPolicyDetailsDeployAgentAction, @@ -49,7 +50,6 @@ import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/rou import { useFormatUrl } from '../../../../common/components/link_to'; import { useAppUrl } from '../../../../common/lib/kibana/hooks'; import { EndpointAction } from '../store/action'; -import { EndpointPolicyLink } from './components/endpoint_policy_link'; import { OutOfDate } from './components/out_of_date'; import { AdminSearchBar } from './components/search_bar'; import { AdministrationListPage } from '../../../components/administration_list_page'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx index 95e08753b9b87c..6b3cc7478079a4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -66,6 +66,7 @@ export const EventFiltersFlyout: React.FC = memo( // load the list of policies> const policiesRequest = useGetEndpointSpecificPolicies({ + perPage: 1000, onError: (error) => { toasts.addWarning(getLoadPoliciesError(error)); }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index adb76683ebb770..ab69fa232dcf83 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -121,6 +121,7 @@ export const EventFiltersListPage = memo(() => { // load the list of policies const policiesRequest = useGetEndpointSpecificPolicies({ + perPage: 1000, onError: (err) => { toasts.addDanger(getLoadPoliciesError(err)); }, diff --git a/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts b/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts index 347f1dc088c10a..c92dcc0bd7cc47 100644 --- a/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/mocks/trusted_apps_http_mocks.ts @@ -33,7 +33,10 @@ import { fleetGetEndpointPackagePolicyListHttpMock, FleetGetEndpointPackagePolicyListHttpMockInterface, } from './fleet_mocks'; -import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../common/endpoint/service/artifacts/constants'; +import { + BY_POLICY_ARTIFACT_TAG_PREFIX, + GLOBAL_ARTIFACT_TAG, +} from '../../../../common/endpoint/service/artifacts/constants'; interface FindExceptionListItemSchemaQueryParams extends Omit { @@ -58,7 +61,11 @@ export const trustedAppsGetListHttpMocks = const generator = new ExceptionsListItemGenerator('seed'); const perPage = apiQueryParams.per_page ?? 10; const data = Array.from({ length: Math.min(perPage, 50) }, () => - generator.generate({ list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, os_types: ['windows'] }) + generator.generate({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + os_types: ['windows'], + tags: [GLOBAL_ARTIFACT_TAG], + }) ); // FIXME: remove hard-coded IDs below adn get them from the new FleetPackagePolicyGenerator (#2262) @@ -130,6 +137,7 @@ export const trustedAppsGetOneHttpMocks = const apiQueryParams = query as ReadExceptionListItemSchema; const exceptionItem = new ExceptionsListItemGenerator('seed').generate({ os_types: ['windows'], + tags: [GLOBAL_ARTIFACT_TAG], }); exceptionItem.item_id = apiQueryParams.item_id ?? exceptionItem.item_id; @@ -157,7 +165,10 @@ export const trustedAppPostHttpMocks = httpHandlerMockFactory(({ policy }) => { const { getAppUrl } = useAppUrl(); const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const policiesRequest = useGetEndpointSpecificPolicies(); + const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx index 3b5244aad30dae..840d2c9f9809d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx @@ -51,7 +51,7 @@ export const PolicyHostIsolationExceptionsList = ({ const { state } = useGetLinkTo(policyId, policyName); // load the list of policies> - const policiesRequest = useGetEndpointSpecificPolicies(); + const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); const [exceptionItemToDelete, setExceptionItemToDelete] = useState< diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx new file mode 100644 index 00000000000000..8c5b355ea05f1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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 { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies'; +import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../services/policies/test_mock_utilts'; +import { PolicyList } from './policy_list'; + +jest.mock('../../../services/policies/policies'); + +const getPackagePolicies = sendGetEndpointSpecificPackagePolicies as jest.Mock; + +describe('When on the policy list page', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = () => (renderResult = mockedContext.render()); + }); + + afterEach(() => { + getPackagePolicies.mockReset(); + }); + + describe('and data exists', () => { + beforeEach(async () => { + getPackagePolicies.mockImplementation(() => sendGetEndpointSpecificPackagePoliciesMock()); + render(); + await waitFor(() => { + expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); + }); + }); + it('should display the policy list table', () => { + expect(renderResult.getByTestId('policyListTable')).toBeTruthy(); + }); + it('should show a link for the policy name', () => { + const policyNameCells = renderResult.getAllByTestId('policyNameCellLink'); + expect(policyNameCells).toBeTruthy(); + expect(policyNameCells.length).toBe(5); + }); + it('should show a avatar for the Created by column', () => { + const createdByCells = renderResult.getAllByTestId('created-by-avatar'); + expect(createdByCells).toBeTruthy(); + expect(createdByCells.length).toBe(5); + }); + it('should show a avatar for the Updated by column', () => { + const updatedByCells = renderResult.getAllByTestId('updated-by-avatar'); + expect(updatedByCells).toBeTruthy(); + expect(updatedByCells.length).toBe(5); + }); + }); + describe('pagination', () => { + beforeEach(async () => { + getPackagePolicies.mockImplementation(async ({ page, perPage }) => { + // # policies = 100 to trigger UI to show pagination + const response = await sendGetEndpointSpecificPackagePoliciesMock({ + page, + perPage, + count: 100, + }); + return response; + }); + render(); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalled(); + }); + }); + afterEach(() => { + getPackagePolicies.mockReset(); + }); + it('should pass the correct page value to the api', async () => { + act(() => { + renderResult.getByTestId('pagination-button-next').click(); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 2, + perPage: 10, + }); + }); + it('should pass the correct pageSize value to the api', async () => { + act(() => { + renderResult.getByTestId('tablePaginationPopoverButton').click(); + }); + const pageSize20 = await renderResult.findByTestId('tablePagination-20-rows'); + act(() => { + pageSize20.click(); + }); + + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 1, + perPage: 20, + }); + }); + it('should call the api with the initial pagination values taken from the url', async () => { + act(() => { + history.push('/administration/policies?page=3&pageSize=50'); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 3, + perPage: 50, + }); + }); + it('should reset page back to 1 if the user is on a page > 1 and they change page size', async () => { + // setup on a different page + act(() => { + history.push('/administration/policies?page=2&pageSize=20'); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + + // change pageSize + act(() => { + renderResult.getByTestId('tablePaginationPopoverButton').click(); + }); + const pageSize10 = await renderResult.findByTestId('tablePagination-10-rows'); + act(() => { + pageSize10.click(); + }); + + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(3); + }); + expect(getPackagePolicies.mock.calls[2][1].query).toEqual({ + page: 1, + perPage: 10, + }); + }); + it('should set page to 1 if user tries to force an invalid page number', async () => { + act(() => { + history.push(`/administration/policies?page=${Number.NEGATIVE_INFINITY}-1&pageSize=20`); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 1, + perPage: 20, + }); + }); + it('should set page size to 10 (management default) if page size is set to anything other than 10, 20, or 50', async () => { + act(() => { + history.push('/administration/policies?page=2&pageSize=13'); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 2, + perPage: 10, + }); + }); + it('should set page to last defined page number value if multiple values exist for page in the URL, i.e. page=2&page=4&page=3 then page is set to 3', async () => { + act(() => { + history.push('/administration/policies?page=2&page=4&page=3&pageSize=10'); + }); + await waitFor(() => { + expect(getPackagePolicies).toHaveBeenCalledTimes(2); + }); + + expect(getPackagePolicies.mock.calls[1][1].query).toEqual({ + page: 3, + perPage: 10, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 472f4a7ba6c6b6..36c43de407104d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -5,16 +5,198 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; +import { + EuiBasicTable, + EuiText, + EuiHorizontalRule, + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + CriteriaWithPagination, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { AdministrationListPage } from '../../../components/administration_list_page'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { EndpointPolicyLink } from '../../../components/endpoint_policy_link'; +import { PolicyData } from '../../../../../common/endpoint/types'; +import { useUrlPagination } from '../../../components/hooks/use_url_pagination'; +import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; export const PolicyList = memo(() => { + const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); + + // load the list of policies + const { data, isFetching, error } = useGetEndpointSpecificPolicies({ + page: pagination.page, + perPage: pagination.pageSize, + }); + + const totalItemCount = data?.total ?? 0; + + const policyColumns = useMemo(() => { + const updatedAtColumnName = i18n.translate('xpack.securitySolution.policy.list.updatedAt', { + defaultMessage: 'Last Updated', + }); + + const createdAtColumnName = i18n.translate('xpack.securitySolution.policy.list.createdAt', { + defaultMessage: 'Date Created', + }); + + return [ + { + field: '', + name: i18n.translate('xpack.securitySolution.policy.list.name', { defaultMessage: 'Name' }), + truncateText: true, + render: (policy: PolicyData) => { + return ( + + + {policy.name} + + + ); + }, + }, + { + field: 'created_by', + name: i18n.translate('xpack.securitySolution.policy.list.createdBy', { + defaultMessage: 'Created by', + }), + truncateText: true, + render: (name: string) => { + return ( + + + + + + {name} + + + ); + }, + }, + { + field: 'created_at', + name: createdAtColumnName, + truncateText: true, + render: (date: string) => { + return ( + + ); + }, + }, + { + field: 'updated_by', + name: i18n.translate('xpack.securitySolution.policy.list.lastUpdatedBy', { + defaultMessage: 'Last updated by', + }), + truncateText: true, + render: (name: string) => { + return ( + + + + + + {name} + + + ); + }, + }, + { + field: 'updated_at', + name: updatedAtColumnName, + truncateText: true, + render: (date: string) => { + return ( + + ); + }, + }, + { + field: '-', + name: i18n.translate('xpack.securitySolution.policy.list.endpoints', { + defaultMessage: 'Endpoints', + }), + }, + { + field: '-', + name: i18n.translate('xpack.securitySolution.policy.list.actions', { + defaultMessage: 'Actions', + }), + }, + ]; + }, []); + + const handleTableOnChange = useCallback( + ({ page }: CriteriaWithPagination) => { + setPagination({ + page: page.index + 1, + pageSize: page.size, + }); + }, + [setPagination] + ); + + const tablePagination = useMemo(() => { + return { + pageIndex: pagination.page - 1, + pageSize: pagination.pageSize, + totalItemCount, + pageSizeOptions, + }; + }, [totalItemCount, pageSizeOptions, pagination.page, pagination.pageSize]); + + const policyListErrorMessage = i18n.translate('xpack.securitySolution.policy.list.errorMessage', { + defaultMessage: 'Error while retrieving list of policies', + }); + return ( + title={i18n.translate('xpack.securitySolution.policy.list.title', { + defaultMessage: 'Policy List', + })} + subtitle={i18n.translate('xpack.securitySolution.policy.list.subtitle', { + defaultMessage: + 'Use endpoint policies to customize endpoint security protections and other configurations', + })} + > + + + + + + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx index 86086deeb58dce..a9aabbdf349800 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx @@ -19,6 +19,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { trustedAppsAllHttpMocks } from '../../../../mocks'; import { HttpFetchOptionsWithPath } from 'kibana/public'; +import { isArtifactByPolicy } from '../../../../../../../common/endpoint/service/artifacts'; jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); @@ -36,6 +37,14 @@ describe('Policy trusted apps flyout', () => { mockedApis = trustedAppsAllHttpMocks(mockedContext.coreStart.http); getState = () => mockedContext.store.getState().management.policyDetails; render = () => mockedContext.render(); + + const getTaListApiResponseMock = + mockedApis.responseProvider.trustedAppsList.getMockImplementation(); + mockedApis.responseProvider.trustedAppsList.mockImplementation((options) => { + const response = getTaListApiResponseMock!(options); + response.data = response.data.filter((ta) => isArtifactByPolicy(ta)); + return response; + }); }); afterEach(() => reactTestingLibrary.cleanup()); @@ -97,7 +106,7 @@ describe('Policy trusted apps flyout', () => { }); expect(component.getByTestId('confirmPolicyTrustedAppsFlyout')).not.toBeNull(); - expect(component.getByTestId('Generated Exception (u6kh2)_checkbox')).not.toBeNull(); + expect(component.getByTestId('Generated Exception (nng74)_checkbox')).not.toBeNull(); }); it('should confirm flyout action', async () => { @@ -111,7 +120,7 @@ describe('Policy trusted apps flyout', () => { }); // TA name below in the selector matches the 3rd generated trusted app which is policy specific - const tACardCheckbox = component.getByTestId('Generated Exception (3xnng)_checkbox'); + const tACardCheckbox = component.getByTestId('Generated Exception (nng74)_checkbox'); act(() => { fireEvent.click(tACardCheckbox); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx index 172b5218188c61..676080d180a6ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx @@ -214,7 +214,7 @@ describe('When using the RemoveTrustedAppFromPolicyModal component', () => { await clickConfirmButton(true, true); expect(appTestContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '"Generated Exception (3xnng)" has been removed from Endpoint Policy policy', + text: '"Generated Exception (nng74)" has been removed from Endpoint Policy policy', title: 'Successfully removed', }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 73d85077e9579e..82169fcd19c104 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -219,7 +219,7 @@ describe('When on the Trusted Apps Page', () => { it('should persist edit params to url', () => { expect(history.location.search).toEqual( - '?show=edit&id=2d95bec3-b48f-4db7-9622-a2b061cc031d' + '?show=edit&id=bec3b48f-ddb7-4622-a2b0-61cc031d17eb' ); }); @@ -251,7 +251,7 @@ describe('When on the Trusted Apps Page', () => { 'addTrustedAppFlyout-createForm-descriptionField' ) as HTMLTextAreaElement; - expect(formNameInput.value).toEqual('Generated Exception (3xnng)'); + expect(formNameInput.value).toEqual('Generated Exception (nng74)'); expect(formDescriptionInput.value).toEqual('created by ExceptionListItemGenerator'); }); @@ -276,8 +276,8 @@ describe('When on the Trusted Apps Page', () => { expect(lastCallToPut[0]).toEqual('/api/exception_lists/items'); expect(JSON.parse(lastCallToPut[1].body as string)).toEqual({ - _version: '3o9za', - name: 'Generated Exception (3xnng)', + _version: '9zawi', + name: 'Generated Exception (nng74)', description: 'created by ExceptionListItemGenerator', entries: [ { @@ -300,7 +300,7 @@ describe('When on the Trusted Apps Page', () => { ], id: '05b5e350-0cad-4dc3-a61d-6e6796b0af39', comments: [], - item_id: '2d95bec3-b48f-4db7-9622-a2b061cc031d', + item_id: 'bec3b48f-ddb7-4622-a2b0-61cc031d17eb', namespace_type: 'agnostic', type: 'simple', }); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index 21a50079a1f1fe..e5c8d110b63f27 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -165,7 +165,8 @@ describe('Exceptions List Api Client', () => { expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); expect(fakeHttpServices.get).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { query: { - id: fakeItemId, + item_id: fakeItemId, + id: undefined, namespace_type: 'agnostic', }, }); @@ -222,7 +223,8 @@ describe('Exceptions List Api Client', () => { expect(fakeHttpServices.delete).toHaveBeenCalledTimes(1); expect(fakeHttpServices.delete).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { query: { - id: fakeItemId, + item_id: fakeItemId, + id: undefined, namespace_type: 'agnostic', }, }); @@ -243,5 +245,32 @@ describe('Exceptions List Api Client', () => { }, }); }); + + it('hasData method returns true when list has data', async () => { + fakeHttpServices.get.mockResolvedValue({ + total: 1, + }); + + const exceptionsListApiClientInstance = getInstance(); + + await expect(exceptionsListApiClientInstance.hasData()).resolves.toBe(true); + + expect(fakeHttpServices.get).toHaveBeenCalledWith(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: expect.objectContaining({ + page: 1, + per_page: 1, + }), + }); + }); + + it('hasData method returns false when list has no data', async () => { + fakeHttpServices.get.mockResolvedValue({ + total: 0, + }); + + const exceptionsListApiClientInstance = getInstance(); + + await expect(exceptionsListApiClientInstance.hasData()).resolves.toBe(false); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index 6edf55e569d355..c995754dd1907c 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -30,7 +30,7 @@ export class ExceptionsListApiClient { constructor( private readonly http: HttpStart, - private readonly listId: ListId, + public readonly listId: ListId, private readonly listDefinition: CreateExceptionListSchema ) { this.ensureListExists = this.createExceptionList(); @@ -166,14 +166,19 @@ export class ExceptionsListApiClient { } /** - * Returns an item filtered by id - * It requires an id in order to get the desired item + * Returns an item for the given `itemId` or `id`. Exception List Items have both an `item_id` + * and `id`, and at least one of these two is required to be provided. */ - async get(id: string): Promise { + async get(itemId?: string, id?: string): Promise { + if (!itemId && !id) { + throw TypeError('either `itemId` or `id` argument must be set'); + } + await this.ensureListExists; return this.http.get(EXCEPTION_LIST_ITEM_URL, { query: { id, + item_id: itemId, namespace_type: 'agnostic', }, }); @@ -204,14 +209,19 @@ export class ExceptionsListApiClient { } /** - * It deletes an existing item. - * It requires a valid item id. + * It deletes an existing item by `itemId` or `id`. Exception List Items have both an `item_id` + * and `id`, and at least one of these two is required to be provided. */ - async delete(id: string): Promise { + async delete(itemId?: string, id?: string): Promise { + if (!itemId && !id) { + throw TypeError('either `itemId` or `id` argument must be set'); + } + await this.ensureListExists; return this.http.delete(EXCEPTION_LIST_ITEM_URL, { query: { id, + item_id: itemId, namespace_type: 'agnostic', }, }); @@ -231,4 +241,11 @@ export class ExceptionsListApiClient { }, }); } + + /** + * Checks if the given list has any data in it + */ + async hasData(): Promise { + return (await this.find({ perPage: 1, page: 1 })).total > 0; + } } diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index 1a0c7ec74d4517..3f3f4b1574c21c 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -7,22 +7,29 @@ import { QueryObserverResult, useQuery } from 'react-query'; import { useHttp } from '../../../common/lib/kibana/hooks'; import { ServerApiError } from '../../../common/types'; +import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; import { GetPolicyListResponse } from '../../pages/policy/types'; import { sendGetEndpointSpecificPackagePolicies } from './policies'; -export function useGetEndpointSpecificPolicies({ - onError, -}: { - onError?: (error: ServerApiError) => void; -} = {}): QueryObserverResult { +export function useGetEndpointSpecificPolicies( + { + onError, + page, + perPage, + }: { + onError?: (error: ServerApiError) => void; + page?: number; + perPage?: number; + } = { page: 1, perPage: MANAGEMENT_DEFAULT_PAGE_SIZE } +): QueryObserverResult { const http = useHttp(); return useQuery( - ['endpointSpecificPolicies'], + ['endpointSpecificPolicies', page, perPage], () => { return sendGetEndpointSpecificPackagePolicies(http, { query: { - page: 1, - perPage: 1000, + page, + perPage, }, }); }, diff --git a/x-pack/plugins/security_solution/public/management/services/policies/test_mock_utilts.ts b/x-pack/plugins/security_solution/public/management/services/policies/test_mock_utilts.ts index 026fb8e243b0bc..354e6ed03eccd5 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/test_mock_utilts.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/test_mock_utilts.ts @@ -7,19 +7,25 @@ import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import { GetPolicyListResponse } from '../../pages/policy/types'; -export const sendGetEndpointSpecificPackagePoliciesMock = - async (): Promise => { - const generator = new FleetPackagePolicyGenerator(); - const items = Array.from({ length: 5 }, (_, index) => { - const policy = generator.generateEndpointPackagePolicy(); - policy.name += ` ${index}`; - return policy; - }); +export const sendGetEndpointSpecificPackagePoliciesMock = async ( + params: { + page: number; + perPage: number; + count: number; + } = { page: 1, perPage: 20, count: 5 } +): Promise => { + const { page, perPage, count } = params; + const generator = new FleetPackagePolicyGenerator(); + const items = Array.from({ length: count }, (_, index) => { + const policy = generator.generateEndpointPackagePolicy(); + policy.name += ` ${index}`; + return policy; + }); - return { - items, - total: 5, - page: 1, - perPage: 10, - }; + return { + items, + total: count, + page, + perPage, }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 86b23594c947a0..bbb3206bf823a5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -12,7 +12,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { TimelineId } from '../../../../../common/types'; -import { useExceptionModal } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_modal'; +import { useExceptionFlyout } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_flyout'; import { AddExceptionFlyoutWrapper } from '../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/flyout'; import { useEventFilterModal } from '../../../../detections/components/alerts_table/timeline_actions/use_event_filter_modal'; @@ -94,12 +94,12 @@ export const EventDetailsFooterComponent = React.memo( }, [timelineId, globalQuery, timelineQuery]); const { - exceptionModalType, + exceptionFlyoutType, onAddExceptionTypeClick, onAddExceptionCancel, onAddExceptionConfirm, ruleIndices, - } = useExceptionModal({ + } = useExceptionFlyout({ ruleIndex, refetch: refetchAll, timelineId, @@ -133,13 +133,13 @@ export const EventDetailsFooterComponent = React.memo( {/* This is still wrong to do render flyout/modal inside of the flyout We need to completely refactor the EventDetails component to be correct */} - {exceptionModalType != null && + {exceptionFlyoutType != null && addExceptionModalWrapperData.ruleId != null && addExceptionModalWrapperData.eventId != null && ( diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts index 70801e08ea335d..7f18c0b40fed7c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -17,8 +17,9 @@ import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; -import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; import { randomPolicyIdGenerator } from '../common/random_policy_id_generator'; +import { ExceptionsListItemGenerator } from '../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { isArtifactByPolicy } from '../../../common/endpoint/service/artifacts'; export const cli = () => { run( @@ -66,7 +67,7 @@ const handleThrowAxiosHttpError = (err: AxiosError): never => { }; const createEventFilters: RunFn = async ({ flags, log }) => { - const eventGenerator = new EventFilterGenerator(); + const eventGenerator = new ExceptionsListItemGenerator(); const kbn = new KbnClient({ log, url: flags.kibana as string }); await ensureCreateEndpointEventFiltersList(kbn); @@ -76,8 +77,9 @@ const createEventFilters: RunFn = async ({ flags, log }) => { await pMap( Array.from({ length: flags.count as unknown as number }), () => { - const body = eventGenerator.generate(); - if (body.tags?.length && body.tags[0] !== 'policy:all') { + const body = eventGenerator.generateEventFilterForCreate(); + + if (isArtifactByPolicy(body)) { const nmExceptions = Math.floor(Math.random() * 3) || 1; body.tags = Array.from({ length: nmExceptions }, () => { return `policy:${randomPolicyId()}`; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index f85c9c33756b71..7425ad0c272c63 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -127,8 +127,6 @@ export function registerSnapshotsRoutes({ operator: searchOperator, }) : '_all', - // @ts-expect-error @elastic/elasticsearch new API params - // https://github.com/elastic/elasticsearch-specification/issues/845 slm_policy_filter: searchField === 'policyName' ? getSnapshotSearchWildcard({ @@ -139,6 +137,7 @@ export function registerSnapshotsRoutes({ }) : '*,_none', order: sortDirection, + // @ts-expect-error sortField: string is not compatible with SnapshotSnapshotSort type sort: sortField, size: pageSize, offset: pageIndex * pageSize, diff --git a/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx b/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx index 63d00f280f3f36..259bd917b5ceac 100644 --- a/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx +++ b/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx @@ -46,7 +46,10 @@ export const TransformAlertFlyout: FC = ({ if (initialAlert) { return triggersActionsUi.getEditAlertFlyout({ ...commonProps, - initialAlert, + initialRule: { + ...initialAlert, + ruleTypeId: initialAlert.alertTypeId, + }, }); } @@ -54,7 +57,7 @@ export const TransformAlertFlyout: FC = ({ ...commonProps, consumer: 'stackAlerts', canChangeTrigger: false, - alertTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, + ruleTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, metadata: {}, initialValues: { params: ruleParams!, diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 7a07a3b0ae6abb..1d73413b3e3867 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -117,6 +117,7 @@ export const useIndexData = ( if (combinedRuntimeMappings !== undefined) { result = Object.keys(combinedRuntimeMappings).map((fieldName) => { const field = combinedRuntimeMappings[fieldName]; + // @ts-expect-error @elastic/elasticsearch does not support yet "composite" type for runtime fields const schema = getDataGridSchemaFromESFieldType(field.type); return { id: fieldName, schema }; }); diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts index e2512bfac2c554..7aebf83b27cca1 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts @@ -29,7 +29,11 @@ interface TestResult { context: TransformHealthAlertContext; } -type Transform = estypes.Transform & { id: string; description?: string; sync: object }; +type Transform = estypes.TransformGetTransformTransformSummary & { + id: string; + description?: string; + sync: object; +}; type TransformWithAlertingRules = Transform & { alerting_rules: TransformHealthAlertRule[] }; diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 09fab2b45909e9..2f82b9a70389b9 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -508,12 +508,9 @@ async function deleteTransforms( transform_id: transformId, }); const transformConfig = body.transforms[0]; - // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform destinationIndex = Array.isArray(transformConfig.dest.index) - ? // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform - transformConfig.dest.index[0] - : // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform - transformConfig.dest.index; + ? transformConfig.dest.index[0] + : transformConfig.dest.index; } catch (getTransformConfigError) { transformDeleted.error = getTransformConfigError.meta.body.error; results[transformId] = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bbbff26238841a..ed110d08a72bf5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1635,10 +1635,7 @@ "core.ui.primaryNav.screenReaderLabel": "プライマリ", "core.ui.primaryNav.toggleNavAriaLabel": "プライマリナビゲーションを切り替える", "core.ui.primaryNavSection.screenReaderLabel": "プライマリナビゲーションリンク、{category}", - "core.ui.publicBaseUrlWarning.configMissingDescription": "{configKey}が見つかりません。本番環境を実行するときに構成してください。一部の機能が正常に動作しない場合があります。", - "core.ui.publicBaseUrlWarning.configMissingTitle": "構成がありません", "core.ui.publicBaseUrlWarning.muteWarningButtonLabel": "ミュート警告", - "core.ui.publicBaseUrlWarning.seeDocumentationLinkLabel": "ドキュメントを参照してください。", "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ:{pageType}", "core.ui.recentlyViewed": "最近閲覧", "core.ui.recentlyViewedAriaLabel": "最近閲覧したリンク", @@ -27595,7 +27592,7 @@ "xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "まとめ", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "このコネクターは Kibana の構成で無効になっています。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "このコネクターには {minimumLicenseRequired} ライセンスが必要です。", - "xpack.triggersActionsUI.checkAlertTypeEnabled.ruleTypeDisabledByLicenseMessage": "このルールタイプには{minimumLicenseRequired}ライセンスが必要です。", + "xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "このルールタイプには{minimumLicenseRequired}ライセンスが必要です。", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "すべてのドキュメント", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel": "トップ", "xpack.triggersActionsUI.common.constants.comparators.isAboveLabel": "より大:", @@ -28043,152 +28040,136 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName}コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "ルールを作成", - "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "フィールドを選択", - "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", - "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "ルールを作成できません。", - "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "ルール\"{ruleName}\"を作成しました", - "xpack.triggersActionsUI.sections.alertAddFooter.cancelButtonLabel": "キャンセル", - "xpack.triggersActionsUI.sections.alertAddFooter.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerEditText": "ルールを編集", - "xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerTitle": "このルールに関連付けられたコネクターの1つで問題が発生しています。", - "xpack.triggersActionsUI.sections.alertDetails.alertDetailsTitle": "{alertName}", - "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule": "このルールは無効になっていて再表示できません。", - "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "無効なルール", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.avgDurationDescription": "平均時間", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert": "アラート", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration": "期間", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start": "開始", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status": "ステータス", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleLastExecutionDescription": "前回の応答", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleTypeExcessDurationMessage": "期間がルールの想定実行時間を超えています。", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active": "アクティブ", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive": "回復済み", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableLoadingTitle": "有効にする", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "有効にする", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteLoadingTitle": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "閉じる", - "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "編集", - "xpack.triggersActionsUI.sections.alertDetails.manageLicensePlanBannerLinkTitle": "ライセンスの管理", - "xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun": "ルール", - "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "ルールを読み込めません:{message}", - "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertsMessage": "アラートを読み込めません:{message}", - "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "アプリで表示", - "xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel": "キャンセル", - "xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle": "このアラートには無効なアクションがあります", - "xpack.triggersActionsUI.sections.alertEdit.flyoutTitle": "ルールを編集", - "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "ルールを更新できません", - "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "'{ruleName}'を更新しました", - "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名前", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.label": "毎", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.description": "アラートステータスが変更されるときにアクションを実行します。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display": "ステータス変更時のみ", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.label": "ステータス変更時のみ", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.description": "アラートがアクティブなときは、ルール間隔でアクションが繰り返されます。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.display": "アラートがアクティブになるたびに実行", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.label": "アラートがアクティブになるたびに実行", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.description": "設定した間隔を使用してアクションを実行します。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.display": "カスタムアクション間隔", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.label": "カスタムアクション間隔", - "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "削除", - "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "確認間隔", - "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "条件を評価する頻度を定義します。チェックはキューに登録されています。可能なかぎり定義された値に近づくように実行されます。", - "xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel": "追加:", - "xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel": "削除", - "xpack.triggersActionsUI.sections.alertForm.conditions.title": "条件:", - "xpack.triggersActionsUI.sections.alertForm.documentationLabel": "ドキュメント", - "xpack.triggersActionsUI.sections.alertForm.error.belowMinimumText": "間隔はこのルールタイプの最小値({minimum})未満です", - "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypes": "ルールを{operation}するには、適切な権限が付与されている必要があります。", - "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypesTitle": "ルールタイプを{operation}する権限がありません。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector": "{actionTypeId}コネクターのアクションが必要です。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "確認間隔が必要です。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "名前が必要です。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText": "ルールタイプは必須です。", - "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypeParamsDescription": "ルールタイプパラメーターを読み込んでいます…", - "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypesDescription": "ルールタイプを読み込んでいます…", - "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知", - "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "ルールがアクティブな間にアクションを繰り返す頻度を定義します。", - "xpack.triggersActionsUI.sections.alertForm.ruleTypeSelectLabel": "ルールタイプを選択", - "xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle": "検索", - "xpack.triggersActionsUI.sections.alertForm.solutionFilterLabel": "ユースケースでフィルタリング", - "xpack.triggersActionsUI.sections.alertForm.tagsFieldLabel": "タグ(任意)", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", - "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", - "xpack.triggersActionsUI.sections.alertsList.addRuleButtonLabel": "ルールを作成", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "ルールの復号中にエラーが発生しました。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDisabled": "ルールを実行できませんでした。ルールは無効化された後に実行されました。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonLicense": "ルールを実行できません", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "ルールの読み取り中にエラーが発生しました。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "ルールの実行中にエラーが発生しました。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonTimeout": "タイムアウトのためルール実行がキャンセルされました。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsWarningTooltip": "このルールに関連付けられたコネクターの1つを読み込めません。ルールを編集して、新しいコネクターを選択します。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "削除", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "削除", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.durationTitle": "ルールを実行するのにかかる時間。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel": "編集", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editButtonTooltip": "編集", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle": "有効", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.lastExecutionDateTitle": "前回の実行の開始時間。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.mutedBadge": "ミュート", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileSelectButton": "パーセンタイルを選択", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileTooltip": "このルールの過去{sampleLimit}実行期間の{percentileOrdinal}パーセンタイル", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "間隔", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "ステータス", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.successRatioTitle": "このルールが正常に実行される頻度", - "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "アクティブ", - "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "エラー", - "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "ステータス", - "xpack.triggersActionsUI.sections.alertsList.alertStatusLicenseError": "ライセンスエラー", - "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "OK", - "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "保留中", - "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "不明", - "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStatusesError, plural, other {# 件のルール}}でエラーが見つかりました。", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "ルールの管理", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "削除", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "無効にする", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.enableAllTitle": "有効にする", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDeleteRulesMessage": "ルールを削除できませんでした", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDisableRulesMessage": "ルールを無効にできませんでした", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToEnableRulesMessage": "ルールを有効にできませんでした", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToMuteRulesMessage": "ルールをミュートできませんでした", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToUnmuteRulesMessage": "ルールをミュート解除できませんでした", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "ミュート", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "ミュート解除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteRuleTitle": "ルールの削除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle": "無効にする", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.editTitle": "ルールを編集", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "有効にする", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.unmuteTitle": "ミュート解除", - "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "閉じる", - "xpack.triggersActionsUI.sections.alertsList.fixLicenseLink": "修正", - "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "ルール", - "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", - "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateTitle": "ルールを作成する権限がありません", - "xpack.triggersActionsUI.sections.alertsList.refreshAlertsButtonLabel": "更新", - "xpack.triggersActionsUI.sections.alertsList.resetDefaultIndexLabel": "デフォルトのインデックスをリセット", - "xpack.triggersActionsUI.sections.alertsList.ruleTypeExcessDurationMessage": "期間がルールの想定実行時間を超えています。", - "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", - "xpack.triggersActionsUI.sections.alertsList.singleTitle": "ルール", - "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "{pageSize}/{totalItemCount}件のルールを表示しています。", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "アクティブ:{totalStatusesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "エラー:{totalStatusesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "Ok:{totalStatusesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "保留:{totalStatusesPending}", - "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "型", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "ルールを読み込めません", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleStatusInfoMessage": "ルールステータス情報を読み込めません", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", - "xpack.triggersActionsUI.sections.alertsList.viewBunnerButtonLabel": "表示", + "xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle": "ルールを作成", + "xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel": "フィールドを選択", + "xpack.triggersActionsUI.sections.ruleAdd.operationName": "作成", + "xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText": "ルールを作成できません。", + "xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText": "ルール\"{ruleName}\"を作成しました", + "xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel": "キャンセル", + "xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText": "ルールを編集", + "xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle": "このルールに関連付けられたコネクターの1つで問題が発生しています。", + "xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}", + "xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRule": "このルールは無効になっていて再表示できません。", + "xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRuleTitle": "無効なルール", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableLoadingTitle": "有効にする", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableTitle": "有効にする", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteLoadingTitle": "ミュート", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteTitle": "ミュート", + "xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle": "閉じる", + "xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "編集", + "xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "ライセンスの管理", + "xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "ルール", + "xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage": "ルールを読み込めません:{message}", + "xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage": "アラートを読み込めません:{message}", + "xpack.triggersActionsUI.sections.ruleDetails.viewRuleInAppButtonLabel": "アプリで表示", + "xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel": "キャンセル", + "xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle": "このアラートには無効なアクションがあります", + "xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle": "ルールを編集", + "xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText": "ルールを更新できません", + "xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText": "'{ruleName}'を更新しました", + "xpack.triggersActionsUI.sections.ruleForm.ruleNameLabel": "名前", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label": "毎", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description": "アラートステータスが変更されるときにアクションを実行します。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display": "ステータス変更時のみ", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label": "ステータス変更時のみ", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description": "アラートがアクティブなときは、ルール間隔でアクションが繰り返されます。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display": "アラートがアクティブになるたびに実行", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label": "アラートがアクティブになるたびに実行", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description": "設定した間隔を使用してアクションを実行します。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display": "カスタムアクション間隔", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label": "カスタムアクション間隔", + "xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel": "削除", + "xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel": "確認間隔", + "xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip": "条件を評価する頻度を定義します。", + "xpack.triggersActionsUI.sections.ruleForm.conditions.addConditionLabel": "追加:", + "xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel": "削除", + "xpack.triggersActionsUI.sections.ruleForm.conditions.title": "条件:", + "xpack.triggersActionsUI.sections.ruleForm.documentationLabel": "ドキュメント", + "xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText": "間隔はこのルールタイプの最小値({minimum})未満です", + "xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypes": "ルールを{operation}するには、適切な権限が付与されている必要があります。", + "xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypesTitle": "ルールタイプを{operation}する権限がありません。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector": "{actionTypeId}コネクターのアクションが必要です。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText": "確認間隔が必要です。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText": "名前が必要です。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText": "ルールタイプは必須です。", + "xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypeParamsDescription": "ルールタイプパラメーターを読み込んでいます…", + "xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription": "ルールタイプを読み込んでいます…", + "xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel": "通知", + "xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip": "ルールがアクティブな間にアクションを繰り返す頻度を定義します。", + "xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel": "ルールタイプを選択", + "xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle": "検索", + "xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel": "ユースケースでフィルタリング", + "xpack.triggersActionsUI.sections.ruleForm.tagsFieldLabel": "タグ(任意)", + "xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", + "xpack.triggersActionsUI.sections.rulesList.actionTypeFilterLabel": "アクションタイプ", + "xpack.triggersActionsUI.sections.rulesList.addRuleButtonLabel": "ルールを作成", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonDecrypting": "ルールの復号中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonLicense": "ルールを実行できません", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonReading": "ルールの読み取り中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonRunning": "ルールの実行中にエラーが発生しました。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonUnknown": "不明な理由でエラーが発生しました。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex": "アクション", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip": "このルールに関連付けられたコネクターの1つを読み込めません。ルールを編集して、新しいコネクターを選択します。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleTypeTitle": "型", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel": "削除", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteButtonTooltip": "削除", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.durationTitle": "ルールを実行するのにかかる時間。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel": "編集", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editButtonTooltip": "編集", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle": "有効", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "前回の実行の開始時間。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.mutedBadge": "ミュート", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名前", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "間隔", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "ステータス", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "アクティブ", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "エラー", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "ステータス", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "ライセンスエラー", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "OK", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "保留中", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusUnknown": "不明", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.buttonTitle": "ルールの管理", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.deleteAllTitle": "削除", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.disableAllTitle": "無効にする", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.enableAllTitle": "有効にする", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage": "ルールを削除できませんでした", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage": "ルールを無効にできませんでした", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage": "ルールを有効にできませんでした", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToMuteRulesMessage": "ルールをミュートできませんでした", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToUnmuteRulesMessage": "ルールをミュート解除できませんでした", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.muteAllTitle": "ミュート", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.unmuteAllTitle": "ミュート解除", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.deleteRuleTitle": "ルールの削除", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle": "無効にする", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle": "ルールを編集", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle": "有効にする", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "ミュート", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "アクション", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "ミュート解除", + "xpack.triggersActionsUI.sections.rulesList.dismissBunnerButtonLabel": "閉じる", + "xpack.triggersActionsUI.sections.rulesList.fixLicenseLink": "修正", + "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "ルール", + "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", + "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle": "ルールを作成する権限がありません", + "xpack.triggersActionsUI.sections.rulesList.refreshRulesButtonLabel": "更新", + "xpack.triggersActionsUI.sections.rulesList.resetDefaultIndexLabel": "デフォルトのインデックスをリセット", + "xpack.triggersActionsUI.sections.rulesList.ruleTypeExcessDurationMessage": "期間がルールの想定実行時間を超えています。", + "xpack.triggersActionsUI.sections.rulesList.searchPlaceholderTitle": "検索", + "xpack.triggersActionsUI.sections.rulesList.singleTitle": "ルール", + "xpack.triggersActionsUI.sections.rulesList.totalItemsCountDescription": "{pageSize}/{totalItemCount}件のルールを表示しています。", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesActiveDescription": "アクティブ:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesErrorDescription": "エラー:{totalStatusesError}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesOkDescription": "Ok:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesPendingDescription": "保留:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.rulesList.typeFilterLabel": "型", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "ルールを読み込めません", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "ルールステータス情報を読み込めません", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", + "xpack.triggersActionsUI.sections.rulesList.viewBunnerButtonLabel": "表示", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Bcc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "認証", @@ -28208,20 +28189,20 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel": "件名", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel": "テナントID", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "ユーザー名", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseCancelButtonText": "キャンセル", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseConfirmButtonText": "変更を破棄", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseMessage": "保存されていない変更は回復できません。", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseTitle": "ルールの保存されていない変更を破棄しますか?", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveCancelButtonText": "キャンセル", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveConfirmButtonText": "ルールを保存", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveTitle": "アクションがないルールを保存しますか?", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveWithoutActionsMessage": "いつでもアクションを追加できます。", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText": "キャンセル", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText": "変更を破棄", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage": "保存されていない変更は回復できません。", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle": "ルールの保存されていない変更を破棄しますか?", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText": "キャンセル", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText": "ルールを保存", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle": "アクションがないルールを保存しますか?", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage": "いつでもアクションを追加できます。", "xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel": "削除", "xpack.triggersActionsUI.sections.connectorAddInline.addConnectorButtonLabel": "コネクターを作成する", "xpack.triggersActionsUI.sections.connectorAddInline.connectorAddInline.actionIdLabel": "別の{connectorInstance}コネクターを使用", "xpack.triggersActionsUI.sections.connectorAddInline.connectorAddInline.addNewConnectorEmptyButton": "コネクターの追加", "xpack.triggersActionsUI.sections.connectorAddInline.emptyConnectorsLabel": "{actionTypeName}コネクターがありません", - "xpack.triggersActionsUI.sections.connectorAddInline.newAlertActionTypeEditTitle": "{actionConnectorName}", + "xpack.triggersActionsUI.sections.connectorAddInline.newRuleActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle": "コネクターを読み込めません", "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "コネクターを読み込めません", "xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。", @@ -28245,7 +28226,7 @@ "xpack.triggersActionsUI.sections.isDeprecatedDescription": "このコネクターは廃止予定です。更新するか新しく作成してください。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseCancelButtonText": "キャンセル", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseConfirmButtonText": "ライセンスの管理", - "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "ルールタイプ{alertTypeId}は無効です。{licenseRequired}ライセンスが必要です。アップグレードオプションを表示するには、[ライセンス管理]に移動してください。", + "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "ルールタイプ{ruleTypeId}は無効です。{licenseRequired}ライセンスが必要です。アップグレードオプションを表示するには、[ライセンス管理]に移動してください。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseTitle": "{licenseRequired}ライセンスが必要です", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "このコネクターはあらかじめ構成されているため、編集できません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 78bd6ec0949b9d..a7c88affdfe8b4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1642,10 +1642,7 @@ "core.ui.primaryNav.screenReaderLabel": "主分片", "core.ui.primaryNav.toggleNavAriaLabel": "切换主导航", "core.ui.primaryNavSection.screenReaderLabel": "主导航链接, {category}", - "core.ui.publicBaseUrlWarning.configMissingDescription": "{configKey} 缺失,在生产环境中运行时应配置。某些功能可能运行不正常。", - "core.ui.publicBaseUrlWarning.configMissingTitle": "配置缺失", "core.ui.publicBaseUrlWarning.muteWarningButtonLabel": "静音警告", - "core.ui.publicBaseUrlWarning.seeDocumentationLinkLabel": "请参阅文档。", "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel},类型:{pageType}", "core.ui.recentlyViewed": "最近查看", "core.ui.recentlyViewedAriaLabel": "最近查看的链接", @@ -27628,7 +27625,7 @@ "xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "摘要", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "连接器已由 Kibana 配置禁用。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "此连接器需要{minimumLicenseRequired}许可证。", - "xpack.triggersActionsUI.checkAlertTypeEnabled.ruleTypeDisabledByLicenseMessage": "此规则类型需要{minimumLicenseRequired}许可证。", + "xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "此规则类型需要{minimumLicenseRequired}许可证。", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel": "所有文档", "xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel": "排名前", "xpack.triggersActionsUI.common.constants.comparators.isAboveLabel": "高于", @@ -28075,153 +28072,138 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建规则", - "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "选择字段", - "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", - "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建规则。", - "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已创建规则“{ruleName}”", - "xpack.triggersActionsUI.sections.alertAddFooter.cancelButtonLabel": "取消", - "xpack.triggersActionsUI.sections.alertAddFooter.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerEditText": "编辑规则", - "xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerTitle": "与此规则关联的连接器之一出现问题。", - "xpack.triggersActionsUI.sections.alertDetails.alertDetailsTitle": "{alertName}", - "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule": "此规则已禁用,无法显示。", - "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "已禁用规则", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.avgDurationDescription": "平均持续时间", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert": "告警", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration": "持续时间", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute": "静音", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start": "启动", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status": "状态", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleLastExecutionDescription": "上次响应", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleTypeExcessDurationMessage": "持续时间超出了规则的预期运行时间。", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active": "活动", - "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive": "已恢复", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableLoadingTitle": "启用", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "启用", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteLoadingTitle": "静音", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", - "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "关闭", - "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "编辑", - "xpack.triggersActionsUI.sections.alertDetails.manageLicensePlanBannerLinkTitle": "管理许可证", - "xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun": "规则", - "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "无法加载规则:{message}", - "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertsMessage": "无法加载告警:{message}", - "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "在应用中查看", - "xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel": "取消", - "xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle": "此规则具有已禁用的操作", - "xpack.triggersActionsUI.sections.alertEdit.flyoutTitle": "编辑规则", - "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "无法更新规则。", - "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "已更新“{ruleName}”", - "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名称", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.label": "每", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.description": "操作在告警状态更改时运行。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display": "仅在状态更改时", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.label": "仅在状态更改时", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.description": "告警活动时,操作按规则时间间隔重复。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.display": "每次告警处于活动状态时", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.label": "每次告警处于活动状态时", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.description": "操作按照您设置的时间间隔运行。", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.display": "按定制操作时间间隔", - "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.label": "按定制操作时间间隔", - "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "删除", - "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "检查频率", - "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "定义评估条件的频率。检查已排队;它们的运行接近于容量允许的定义值。", - "xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel": "添加:", - "xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel": "移除", - "xpack.triggersActionsUI.sections.alertForm.conditions.title": "条件:", - "xpack.triggersActionsUI.sections.alertForm.documentationLabel": "文档", - "xpack.triggersActionsUI.sections.alertForm.error.belowMinimumText": "时间间隔小于此规则类型的最小值 ({minimum})", - "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypes": "为了{operation}规则,您需要获得相应的权限。", - "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypesTitle": "您尚无权{operation}任何规则类型", - "xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector": "“{actionTypeId} 连接器的操作”必填。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "“检查时间间隔”必填。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "“名称”必填。", - "xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText": "“规则类型”必填。", - "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypeParamsDescription": "正在加载规则类型参数……", - "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypesDescription": "正在加载规则类型……", - "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知", - "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义规则处于活动状态时重复操作的频率。", - "xpack.triggersActionsUI.sections.alertForm.ruleTypeSelectLabel": "选择规则类型", - "xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle": "搜索", - "xpack.triggersActionsUI.sections.alertForm.solutionFilterLabel": "按用例筛选", - "xpack.triggersActionsUI.sections.alertForm.tagsFieldLabel": "标签(可选)", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadRuleTypesMessage": "无法加载规则类型", - "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", - "xpack.triggersActionsUI.sections.alertsList.addRuleButtonLabel": "创建规则", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "解密规则时发生错误。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDisabled": "无法执行规则,因为在规则禁用之后已运行。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonLicense": "无法运行规则", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "读取规则时发生错误。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行规则时发生错误。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonTimeout": "由于超时,规则执行已取消。", - "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsWarningTooltip": "无法加载与此规则关联的连接器之一。请编辑该规则以选择新连接器。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "删除", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "删除", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.durationTitle": "运行规则所需的时间长度。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel": "编辑", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editButtonTooltip": "编辑", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle": "已启用", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.lastExecutionDateTitle": "上次执行的开始时间。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.mutedBadge": "已静音", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileSelectButton": "选择百分位数", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileTooltip": "此规则过去的 {sampleLimit} 执行持续时间的第 {percentileOrdinal} 个百分位", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "时间间隔", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "状态", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.successRatioTitle": "成功执行此规则的频率", - "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "活动", - "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "错误", - "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "状态", - "xpack.triggersActionsUI.sections.alertsList.alertStatusLicenseError": "许可证错误", - "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "确定", - "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "待处理", - "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "未知", - "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStatusesError, plural, other {# 个规则}}中出现错误。", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "管理规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "删除", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "禁用", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.enableAllTitle": "启用", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDeleteRulesMessage": "无法删除规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDisableRulesMessage": "无法禁用规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToEnableRulesMessage": "无法启用规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToMuteRulesMessage": "无法静音规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToUnmuteRulesMessage": "无法取消静音规则", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "静音", - "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "取消静音", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteRuleTitle": "删除规则", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle": "禁用", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.editTitle": "编辑规则", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "启用", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.unmuteTitle": "取消静音", - "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "关闭", - "xpack.triggersActionsUI.sections.alertsList.fixLicenseLink": "修复", - "xpack.triggersActionsUI.sections.alertsList.multipleTitle": "规则", - "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateDescription": "请联系您的系统管理员。", - "xpack.triggersActionsUI.sections.alertsList.noPermissionToCreateTitle": "没有创建规则的权限", - "xpack.triggersActionsUI.sections.alertsList.refreshAlertsButtonLabel": "刷新", - "xpack.triggersActionsUI.sections.alertsList.resetDefaultIndexLabel": "重置默认索引", - "xpack.triggersActionsUI.sections.alertsList.ruleTypeExcessDurationMessage": "持续时间超出了规则的预期运行时间。", - "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", - "xpack.triggersActionsUI.sections.alertsList.singleTitle": "规则", - "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "正在显示:{pageSize} 个规则(共 {totalItemCount} 个)。", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "活动:{totalStatusesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "错误:{totalStatusesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "确定:{totalStatusesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "待处理:{totalStatusesPending}", - "xpack.triggersActionsUI.sections.alertsList.totalStatusesUnknownDescription": "未知:{totalStatusesUnknown}", - "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "类型", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "无法加载连接器类型", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "无法加载规则", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleStatusInfoMessage": "无法加载规则状态信息", - "xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleTypesMessage": "无法加载规则类型", - "xpack.triggersActionsUI.sections.alertsList.viewBunnerButtonLabel": "查看", + "xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle": "创建规则", + "xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel": "选择字段", + "xpack.triggersActionsUI.sections.ruleAdd.operationName": "创建", + "xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText": "无法创建规则。", + "xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText": "已创建规则“{ruleName}”", + "xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel": "取消", + "xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText": "编辑规则", + "xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle": "与此规则关联的连接器之一出现问题。", + "xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}", + "xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRule": "此规则已禁用,无法显示。", + "xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRuleTitle": "已禁用规则", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableLoadingTitle": "启用", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableTitle": "启用", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteLoadingTitle": "静音", + "xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteTitle": "静音", + "xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle": "关闭", + "xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "编辑", + "xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "管理许可证", + "xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "规则", + "xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage": "无法加载规则:{message}", + "xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage": "无法加载告警:{message}", + "xpack.triggersActionsUI.sections.ruleDetails.viewRuleInAppButtonLabel": "在应用中查看", + "xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel": "取消", + "xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle": "此规则具有已禁用的操作", + "xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle": "编辑规则", + "xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel": "保存", + "xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText": "无法更新规则。", + "xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText": "已更新“{ruleName}”", + "xpack.triggersActionsUI.sections.ruleForm.ruleNameLabel": "名称", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label": "每", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description": "操作在告警状态更改时运行。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display": "仅在状态更改时", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label": "仅在状态更改时", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description": "告警活动时,操作按规则时间间隔重复。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display": "每次告警处于活动状态时", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label": "每次告警处于活动状态时", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description": "操作按照您设置的时间间隔运行。", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display": "按定制操作时间间隔", + "xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label": "按定制操作时间间隔", + "xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel": "删除", + "xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel": "检查频率", + "xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip": "定义评估条件的频率。", + "xpack.triggersActionsUI.sections.ruleForm.conditions.addConditionLabel": "添加:", + "xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel": "移除", + "xpack.triggersActionsUI.sections.ruleForm.conditions.title": "条件:", + "xpack.triggersActionsUI.sections.ruleForm.documentationLabel": "文档", + "xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText": "时间间隔小于此规则类型的最小值 ({minimum})", + "xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypes": "为了{operation}规则,您需要获得相应的权限。", + "xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypesTitle": "您尚无权{operation}任何规则类型", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector": "“{actionTypeId} 连接器的操作”必填。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText": "“检查时间间隔”必填。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText": "“名称”必填。", + "xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText": "“规则类型”必填。", + "xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypeParamsDescription": "正在加载规则类型参数……", + "xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription": "正在加载规则类型……", + "xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel": "通知", + "xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip": "定义规则处于活动状态时重复操作的频率。", + "xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel": "选择规则类型", + "xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle": "搜索", + "xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel": "按用例筛选", + "xpack.triggersActionsUI.sections.ruleForm.tagsFieldLabel": "标签(可选)", + "xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage": "无法加载规则类型", + "xpack.triggersActionsUI.sections.rulesList.actionTypeFilterLabel": "操作类型", + "xpack.triggersActionsUI.sections.rulesList.addRuleButtonLabel": "创建规则", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonDecrypting": "解密规则时发生错误。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonLicense": "无法运行规则", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonReading": "读取规则时发生错误。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonRunning": "运行规则时发生错误。", + "xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonUnknown": "由于未知原因发生错误。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex": "操作", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip": "无法加载与此规则关联的连接器之一。请编辑该规则以选择新连接器。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleTypeTitle": "类型", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel": "删除", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteButtonTooltip": "删除", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.durationTitle": "运行规则所需的时间长度。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel": "编辑", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editButtonTooltip": "编辑", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle": "已启用", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "上次执行的开始时间。", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.mutedBadge": "已静音", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名称", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "时间间隔", + "xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "状态", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "活动", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "错误", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "状态", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "许可证错误", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "确定", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "待处理", + "xpack.triggersActionsUI.sections.rulesList.ruleStatusUnknown": "未知", + "xpack.triggersActionsUI.sections.rulesList.attentionBannerTitle": "{totalStatusesError, plural, other {# 个规则}}中出现错误。", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.buttonTitle": "管理规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.deleteAllTitle": "删除", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.disableAllTitle": "禁用", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.enableAllTitle": "启用", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage": "无法删除规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage": "无法禁用规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage": "无法启用规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToMuteRulesMessage": "无法静音规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToUnmuteRulesMessage": "无法取消静音规则", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.muteAllTitle": "静音", + "xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.unmuteAllTitle": "取消静音", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.deleteRuleTitle": "删除规则", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle": "禁用", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle": "编辑规则", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle": "启用", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "静音", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "操作", + "xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "取消静音", + "xpack.triggersActionsUI.sections.rulesList.dismissBunnerButtonLabel": "关闭", + "xpack.triggersActionsUI.sections.rulesList.fixLicenseLink": "修复", + "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "规则", + "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "请联系您的系统管理员。", + "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle": "没有创建规则的权限", + "xpack.triggersActionsUI.sections.rulesList.refreshRulesButtonLabel": "刷新", + "xpack.triggersActionsUI.sections.rulesList.resetDefaultIndexLabel": "重置默认索引", + "xpack.triggersActionsUI.sections.rulesList.ruleTypeExcessDurationMessage": "持续时间超出了规则的预期运行时间。", + "xpack.triggersActionsUI.sections.rulesList.searchPlaceholderTitle": "搜索", + "xpack.triggersActionsUI.sections.rulesList.singleTitle": "规则", + "xpack.triggersActionsUI.sections.rulesList.totalItemsCountDescription": "正在显示:{pageSize} 个规则(共 {totalItemCount} 个)。", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesActiveDescription": "活动:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesErrorDescription": "错误:{totalStatusesError}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesOkDescription": "确定:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesPendingDescription": "待处理:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.rulesList.totalStatusesUnknownDescription": "未知:{totalStatusesUnknown}", + "xpack.triggersActionsUI.sections.rulesList.typeFilterLabel": "类型", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadConnectorTypesMessage": "无法加载连接器类型", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "无法加载规则", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "无法加载规则状态信息", + "xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "无法加载规则类型", + "xpack.triggersActionsUI.sections.rulesList.viewBunnerButtonLabel": "查看", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "密送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "抄送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "身份验证", @@ -28241,20 +28223,20 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel": "主题", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel": "租户 ID", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "用户名", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseCancelButtonText": "取消", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseConfirmButtonText": "放弃更改", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseMessage": "您无法恢复未保存更改。", - "xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseTitle": "丢弃规则的未保存更改?", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveCancelButtonText": "取消", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveConfirmButtonText": "保存规则", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveTitle": "保存规则,而不执行任何操作?", - "xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveWithoutActionsMessage": "您可以随时添加操作。", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText": "取消", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText": "放弃更改", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage": "您无法恢复未保存更改。", + "xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle": "丢弃规则的未保存更改?", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText": "取消", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText": "保存规则", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle": "保存规则,而不执行任何操作?", + "xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage": "您可以随时添加操作。", "xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel": "删除", "xpack.triggersActionsUI.sections.connectorAddInline.addConnectorButtonLabel": "创建连接器", "xpack.triggersActionsUI.sections.connectorAddInline.connectorAddInline.actionIdLabel": "使用其他 {connectorInstance} 连接器", "xpack.triggersActionsUI.sections.connectorAddInline.connectorAddInline.addNewConnectorEmptyButton": "添加连接器", "xpack.triggersActionsUI.sections.connectorAddInline.emptyConnectorsLabel": "无 {actionTypeName} 连接器", - "xpack.triggersActionsUI.sections.connectorAddInline.newAlertActionTypeEditTitle": "{actionConnectorName}", + "xpack.triggersActionsUI.sections.connectorAddInline.newRuleActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle": "无法加载连接器", "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "无法加载连接器", "xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。", @@ -28278,7 +28260,7 @@ "xpack.triggersActionsUI.sections.isDeprecatedDescription": "此连接器已过时。请进行更新,或创建新连接器。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseCancelButtonText": "取消", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseConfirmButtonText": "管理许可证", - "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "规则类型 {alertTypeId} 已禁用,因为它需要{licenseRequired}许可证。继续前往“许可证管理”查看升级选项。", + "xpack.triggersActionsUI.sections.manageLicense.manageLicenseMessage": "规则类型 {ruleTypeId} 已禁用,因为它需要{licenseRequired}许可证。继续前往“许可证管理”查看升级选项。", "xpack.triggersActionsUI.sections.manageLicense.manageLicenseTitle": "需要{licenseRequired}许可证", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "这是预配置连接器,无法编辑", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 504f9256d02838..b2c350a4f1f290 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -29,8 +29,8 @@ import { setSavedObjectsClient } from '../common/lib/data_apis'; import { KibanaContextProvider } from '../common/lib/kibana'; const TriggersActionsUIHome = lazy(() => import('./home')); -const AlertDetailsRoute = lazy( - () => import('./sections/alert_details/components/alert_details_route') +const RuleDetailsRoute = lazy( + () => import('./sections/rule_details/components/rule_details_route') ); export interface TriggersAndActionsUiServices extends CoreStart { @@ -88,7 +88,7 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index c2946d0d5fb155..a488086032f075 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -20,7 +20,7 @@ import { useHealthContext } from '../context/health_context'; import { useKibana } from '../../common/lib/kibana'; import { CenterJustifiedSpinner } from './center_justified_spinner'; import { triggersActionsUiHealth } from '../../common/lib/health_api'; -import { alertingFrameworkHealth } from '../lib/alert_api'; +import { alertingFrameworkHealth } from '../lib/rule_api'; interface Props { inFlyout?: boolean; @@ -28,7 +28,7 @@ interface Props { } interface HealthStatus { - isAlertsAvailable: boolean; + isRulesAvailable: boolean; isSufficientlySecure: boolean; hasPermanentEncryptionKey: boolean; } @@ -51,7 +51,7 @@ export const HealthCheck: React.FunctionComponent = ({ isSufficientlySecure: false, hasPermanentEncryptionKey: false, }; - if (healthStatus.isAlertsAvailable) { + if (healthStatus.isRulesAvailable) { const alertingHealthResult = await alertingFrameworkHealth({ http }); healthStatus.isSufficientlySecure = alertingHealthResult.isSufficientlySecure; healthStatus.hasPermanentEncryptionKey = alertingHealthResult.hasPermanentEncryptionKey; @@ -79,7 +79,7 @@ export const HealthCheck: React.FunctionComponent = ({ (healthCheck) => { return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( <>{children} - ) : !healthCheck.isAlertsAvailable ? ( + ) : !healthCheck.isRulesAvailable ? ( ) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx index aef842c9baccbe..c9f010b2d6a7e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx @@ -12,7 +12,7 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( void }) => ( } actions={ import('./sections/actions_connectors_list/components/actions_connectors_list') ); -const AlertsList = lazy(() => import('./sections/alerts_list/components/alerts_list')); +const RulesList = lazy(() => import('./sections/rules_list/components/rules_list')); export interface MatchParams { section: Section; @@ -132,7 +132,7 @@ export const TriggersActionsUIHome: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts deleted file mode 100644 index ea4f146cb6acbc..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { alertingFrameworkHealth } from './health'; -export { mapFiltersToKql } from './map_filters_to_kql'; -export { loadAlertAggregations } from './aggregate'; -export { createAlert } from './create'; -export { deleteAlerts } from './delete'; -export { disableAlert, disableAlerts } from './disable'; -export { enableAlert, enableAlerts } from './enable'; -export { loadAlert } from './get_rule'; -export { loadAlertSummary } from './alert_summary'; -export { muteAlertInstance } from './mute_alert'; -export { muteAlert, muteAlerts } from './mute'; -export { loadAlertTypes } from './rule_types'; -export { loadAlerts } from './rules'; -export { loadAlertState } from './state'; -export { unmuteAlertInstance } from './unmute_alert'; -export { unmuteAlert, unmuteAlerts } from './unmute'; -export { updateAlert } from './update'; -export { resolveRule } from './resolve_rule'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts index 4dbda8f5d96143..26a69093ae48d2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from './breadcrumb'; +import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from './breadcrumb'; import { i18n } from '@kbn/i18n'; import { routeToConnectors, routeToRules, routeToHome } from '../constants'; @@ -32,9 +32,9 @@ describe('getAlertingSectionBreadcrumb', () => { }); }); -describe('getAlertDetailsBreadcrumb', () => { +describe('getRuleDetailsBreadcrumb', () => { test('if select an alert should return proper breadcrumb title with alert name ', async () => { - expect(getAlertDetailsBreadcrumb('testId', 'testName')).toMatchObject({ + expect(getRuleDetailsBreadcrumb('testId', 'testName')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.alertDetails.breadcrumbTitle', { defaultMessage: 'testName', }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts index b98aac8719d32b..d26cdbcb3d1744 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts @@ -35,7 +35,7 @@ export const getAlertingSectionBreadcrumb = (type: string): { text: string; href } }; -export const getAlertDetailsBreadcrumb = ( +export const getRuleDetailsBreadcrumb = ( id: string, name: string ): { text: string; href: string } => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 0cde499de8042c..74b8243519428b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -6,7 +6,7 @@ */ import { RuleType } from '../../types'; -import { InitialAlert } from '../sections/alert_form/alert_reducer'; +import { InitialRule } from '../sections/rule_form/rule_reducer'; /** * NOTE: Applications that want to show the alerting UIs will need to add @@ -23,9 +23,9 @@ export const hasExecuteActionsCapability = (capabilities: Capabilities) => export const hasDeleteActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.delete; -export function hasAllPrivilege(alert: InitialAlert, alertType?: RuleType): boolean { - return alertType?.authorizedConsumers[alert.consumer]?.all ?? false; +export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { + return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; } -export function hasReadPrivilege(alert: InitialAlert, alertType?: RuleType): boolean { - return alertType?.authorizedConsumers[alert.consumer]?.read ?? false; +export function hasReadPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { + return ruleType?.authorizedConsumers[rule.consumer]?.read ?? false; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.test.tsx similarity index 75% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.test.tsx index ebdea0c3e58789..0668fb56c4ef19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.test.tsx @@ -6,18 +6,18 @@ */ import { RuleType } from '../../types'; -import { checkAlertTypeEnabled } from './check_alert_type_enabled'; +import { checkRuleTypeEnabled } from './check_rule_type_enabled'; -describe('checkAlertTypeEnabled', () => { - test(`returns isEnabled:true when alert type isn't provided`, async () => { - expect(checkAlertTypeEnabled()).toMatchInlineSnapshot(` +describe('checkRuleTypeEnabled', () => { + test(`returns isEnabled:true when rule type isn't provided`, async () => { + expect(checkRuleTypeEnabled()).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); }); - test('returns isEnabled:true when alert type is enabled', async () => { + test('returns isEnabled:true when rule type is enabled', async () => { const alertType: RuleType = { id: 'test', name: 'Test', @@ -34,14 +34,14 @@ describe('checkAlertTypeEnabled', () => { minimumLicenseRequired: 'basic', enabledInLicense: true, }; - expect(checkAlertTypeEnabled(alertType)).toMatchInlineSnapshot(` + expect(checkRuleTypeEnabled(alertType)).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); }); - test('returns isEnabled:false when alert type is disabled by license', async () => { + test('returns isEnabled:false when rule type is disabled by license', async () => { const alertType: RuleType = { id: 'test', name: 'Test', @@ -58,7 +58,7 @@ describe('checkAlertTypeEnabled', () => { minimumLicenseRequired: 'gold', enabledInLicense: false, }; - expect(checkAlertTypeEnabled(alertType)).toMatchInlineSnapshot(` + expect(checkRuleTypeEnabled(alertType)).toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This rule type requires a Gold license.", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.tsx similarity index 64% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.tsx index 3b53e8f9a6c336..b1127c0b90eed2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_rule_type_enabled.tsx @@ -17,24 +17,24 @@ export interface IsDisabledResult { message: string; } -const getLicenseCheckResult = (alertType: RuleType) => { +const getLicenseCheckResult = (ruleType: RuleType) => { return { isEnabled: false, message: i18n.translate( - 'xpack.triggersActionsUI.checkAlertTypeEnabled.ruleTypeDisabledByLicenseMessage', + 'xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage', { defaultMessage: 'This rule type requires a {minimumLicenseRequired} license.', values: { - minimumLicenseRequired: upperFirst(alertType.minimumLicenseRequired), + minimumLicenseRequired: upperFirst(ruleType.minimumLicenseRequired), }, } ), }; }; -export function checkAlertTypeEnabled(alertType?: RuleType): IsEnabledResult | IsDisabledResult { - if (alertType?.enabledInLicense === false) { - return getLicenseCheckResult(alertType); +export function checkRuleTypeEnabled(ruleType?: RuleType): IsEnabledResult | IsDisabledResult { + if (ruleType?.enabledInLicense === false) { + return getLicenseCheckResult(ruleType); } return { isEnabled: true }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts similarity index 90% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 57feb1e7abae97..53378d1af572dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -6,11 +6,11 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { loadAlertAggregations } from './aggregate'; +import { loadRuleAggregations } from './aggregate'; const http = httpServiceMock.createStartContract(); -describe('loadAlertAggregations', () => { +describe('loadRuleAggregations', () => { beforeEach(() => jest.resetAllMocks()); test('should call aggregate API with base parameters', async () => { @@ -25,9 +25,9 @@ describe('loadAlertAggregations', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertAggregations({ http }); + const result = await loadRuleAggregations({ http }); expect(result).toEqual({ - alertExecutionStatus: { + ruleExecutionStatus: { ok: 4, active: 2, error: 1, @@ -62,9 +62,9 @@ describe('loadAlertAggregations', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertAggregations({ http, searchText: 'apples' }); + const result = await loadRuleAggregations({ http, searchText: 'apples' }); expect(result).toEqual({ - alertExecutionStatus: { + ruleExecutionStatus: { ok: 4, active: 2, error: 1, @@ -99,13 +99,13 @@ describe('loadAlertAggregations', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertAggregations({ + const result = await loadRuleAggregations({ http, searchText: 'foo', actionTypesFilter: ['action', 'type'], }); expect(result).toEqual({ - alertExecutionStatus: { + ruleExecutionStatus: { ok: 4, active: 2, error: 1, @@ -140,12 +140,12 @@ describe('loadAlertAggregations', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertAggregations({ + const result = await loadRuleAggregations({ http, typesFilter: ['foo', 'bar'], }); expect(result).toEqual({ - alertExecutionStatus: { + ruleExecutionStatus: { ok: 4, active: 2, error: 1, @@ -180,14 +180,14 @@ describe('loadAlertAggregations', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertAggregations({ + const result = await loadRuleAggregations({ http, searchText: 'baz', actionTypesFilter: ['action', 'type'], typesFilter: ['foo', 'bar'], }); expect(result).toEqual({ - alertExecutionStatus: { + ruleExecutionStatus: { ok: 4, active: 2, error: 1, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 60482b26b7f259..b2556900e763a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -5,38 +5,38 @@ * 2.0. */ import { HttpSetup } from 'kibana/public'; -import { AlertAggregations } from '../../../types'; +import { RuleAggregations } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; -const rewriteBodyRes: RewriteRequestCase = ({ - rule_execution_status: alertExecutionStatus, +const rewriteBodyRes: RewriteRequestCase = ({ + rule_execution_status: ruleExecutionStatus, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, ...rest }: any) => ({ ...rest, - alertExecutionStatus, + ruleExecutionStatus, ruleEnabledStatus, ruleMutedStatus, }); -export async function loadAlertAggregations({ +export async function loadRuleAggregations({ http, searchText, typesFilter, actionTypesFilter, - alertStatusesFilter, + ruleStatusesFilter, }: { http: HttpSetup; searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - const res = await http.get>( + ruleStatusesFilter?: string[]; +}): Promise { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter }); + const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { query: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts similarity index 82% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts index d380665658a81c..bf2a0662490ae3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts @@ -6,9 +6,9 @@ */ import { AlertExecutionStatus } from '../../../../../alerting/common'; import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; -import { Rule, AlertAction, ResolvedRule } from '../../../types'; +import { Rule, RuleAction, ResolvedRule } from '../../../types'; -const transformAction: RewriteRequestCase = ({ +const transformAction: RewriteRequestCase = ({ group, id, connector_type_id: actionTypeId, @@ -30,8 +30,8 @@ const transformExecutionStatus: RewriteRequestCase = ({ ...rest, }); -export const transformAlert: RewriteRequestCase = ({ - rule_type_id: alertTypeId, +export const transformRule: RewriteRequestCase = ({ + rule_type_id: ruleTypeId, created_by: createdBy, updated_by: updatedBy, created_at: createdAt, @@ -45,7 +45,7 @@ export const transformAlert: RewriteRequestCase = ({ actions: actions, ...rest }: any) => ({ - alertTypeId, + ruleTypeId, createdBy, updatedBy, createdAt, @@ -56,7 +56,7 @@ export const transformAlert: RewriteRequestCase = ({ mutedInstanceIds, executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, actions: actions - ? actions.map((action: AsApiContract) => transformAction(action)) + ? actions.map((action: AsApiContract) => transformAction(action)) : [], scheduledTaskId, ...rest, @@ -69,7 +69,7 @@ export const transformResolvedRule: RewriteRequestCase = ({ ...rest }: any) => { return { - ...transformAlert(rest), + ...transformRule(rest), alias_target_id, outcome, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts similarity index 92% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts index 8d1ec57a4e63ee..acba66c83a7358 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts @@ -6,12 +6,12 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { AlertUpdates } from '../../../types'; -import { createAlert } from './create'; +import { RuleUpdates } from '../../../types'; +import { createRule } from './create'; const http = httpServiceMock.createStartContract(); -describe('createAlert', () => { +describe('createRule', () => { beforeEach(() => jest.resetAllMocks()); test('should call create alert API', async () => { @@ -49,8 +49,8 @@ describe('createAlert', () => { create_at: '2021-04-01T21:33:13.247Z', updated_at: '2021-04-01T21:33:13.247Z', }; - const alertToCreate: Omit< - AlertUpdates, + const ruleToCreate: Omit< + RuleUpdates, 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' > = { params: { @@ -70,7 +70,7 @@ describe('createAlert', () => { name: 'test', enabled: true, throttle: null, - alertTypeId: '.index-threshold', + ruleTypeId: '.index-threshold', notifyWhen: 'onActionGroupChange', actions: [ { @@ -90,7 +90,7 @@ describe('createAlert', () => { }; http.post.mockResolvedValueOnce(resolvedValue); - const result = await createAlert({ http, alert: alertToCreate }); + const result = await createRule({ http, rule: ruleToCreate }); expect(result).toEqual({ actions: [ { @@ -103,7 +103,7 @@ describe('createAlert', () => { }, }, ], - alertTypeId: '.index-threshold', + ruleTypeId: '.index-threshold', apiKeyOwner: undefined, consumer: 'alerts', create_at: '2021-04-01T21:33:13.247Z', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts similarity index 66% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts index d07d99eb8e8c80..26b6cf6a9628c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts @@ -6,22 +6,22 @@ */ import { HttpSetup } from 'kibana/public'; import { AsApiContract, RewriteResponseCase } from '../../../../../actions/common'; -import { Rule, AlertUpdates } from '../../../types'; +import { Rule, RuleUpdates } from '../../../types'; import { BASE_ALERTING_API_PATH } from '../../constants'; -import { transformAlert } from './common_transformations'; +import { transformRule } from './common_transformations'; -type AlertCreateBody = Omit< - AlertUpdates, +type RuleCreateBody = Omit< + RuleUpdates, 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' >; -const rewriteBodyRequest: RewriteResponseCase = ({ - alertTypeId, +const rewriteBodyRequest: RewriteResponseCase = ({ + ruleTypeId, notifyWhen, actions, ...res }): any => ({ ...res, - rule_type_id: alertTypeId, + rule_type_id: ruleTypeId, notify_when: notifyWhen, actions: actions.map(({ group, id, params }) => ({ group, @@ -30,15 +30,15 @@ const rewriteBodyRequest: RewriteResponseCase = ({ })), }); -export async function createAlert({ +export async function createRule({ http, - alert, + rule, }: { http: HttpSetup; - alert: AlertCreateBody; + rule: RuleCreateBody; }): Promise { const res = await http.post>(`${BASE_ALERTING_API_PATH}/rule`, { - body: JSON.stringify(rewriteBodyRequest(alert)), + body: JSON.stringify(rewriteBodyRequest(rule)), }); - return transformAlert(res); + return transformRule(res); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts index 11e5f4763e775e..95e48cf6e1734e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts @@ -6,14 +6,14 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { deleteAlerts } from './delete'; +import { deleteRules } from './delete'; const http = httpServiceMock.createStartContract(); -describe('deleteAlerts', () => { +describe('deleteRules', () => { test('should call delete API for each alert', async () => { const ids = ['1', '2', '/']; - const result = await deleteAlerts({ http, ids }); + const result = await deleteRules({ http, ids }); expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); expect(http.delete.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts similarity index 95% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts index d223dd08ca29a5..053f5d68cdb697 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts @@ -7,7 +7,7 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ALERTING_API_PATH } from '../../constants'; -export async function deleteAlerts({ +export async function deleteRules({ ids, http, }: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.test.ts similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.test.ts index 4323816221c6ed..582fb3b59f86f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.test.ts @@ -6,14 +6,14 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { disableAlert, disableAlerts } from './disable'; +import { disableRule, disableRules } from './disable'; const http = httpServiceMock.createStartContract(); beforeEach(() => jest.resetAllMocks()); -describe('disableAlert', () => { - test('should call disable alert API', async () => { - const result = await disableAlert({ http, id: '1/' }); +describe('disableRule', () => { + test('should call disable rule API', async () => { + const result = await disableRule({ http, id: '1/' }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ @@ -25,10 +25,10 @@ describe('disableAlert', () => { }); }); -describe('disableAlerts', () => { - test('should call disable alert API per alert', async () => { +describe('disableRules', () => { + test('should call disable rule API per rule', async () => { const ids = ['1', '2', '/']; - const result = await disableAlerts({ http, ids }); + const result = await disableRules({ http, ids }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.ts similarity index 73% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.ts index 4bb3e3d45fcaea..5b1e0452f51c13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/disable.ts @@ -7,16 +7,16 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ALERTING_API_PATH } from '../../constants'; -export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_enable`); +export async function disableRule({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_disable`); } -export async function enableAlerts({ +export async function disableRules({ ids, http, }: { ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map((id) => enableAlert({ id, http }))); + await Promise.all(ids.map((id) => disableRule({ id, http }))); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.test.ts similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.test.ts index 3a54a0772664b8..993ae31f288328 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.test.ts @@ -6,14 +6,14 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { enableAlert, enableAlerts } from './enable'; +import { enableRule, enableRules } from './enable'; const http = httpServiceMock.createStartContract(); beforeEach(() => jest.resetAllMocks()); -describe('enableAlert', () => { - test('should call enable alert API', async () => { - const result = await enableAlert({ http, id: '1/' }); +describe('enableRule', () => { + test('should call enable rule API', async () => { + const result = await enableRule({ http, id: '1/' }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ @@ -25,10 +25,10 @@ describe('enableAlert', () => { }); }); -describe('enableAlerts', () => { - test('should call enable alert API per alert', async () => { +describe('enableRules', () => { + test('should call enable rule API per rule', async () => { const ids = ['1', '2', '/']; - const result = await enableAlerts({ http, ids }); + const result = await enableRules({ http, ids }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.ts similarity index 68% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.ts index 758e66644b34e7..fa4a4629566630 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/enable.ts @@ -7,16 +7,16 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ALERTING_API_PATH } from '../../constants'; -export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_disable`); +export async function enableRule({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_enable`); } -export async function disableAlerts({ +export async function enableRules({ ids, http, }: { ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map((id) => disableAlert({ id, http }))); + await Promise.all(ids.map((id) => enableRule({ id, http }))); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.test.ts similarity index 90% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.test.ts index 4708772f4ec35f..9d7a3c5d5e6f54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.test.ts @@ -6,15 +6,15 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { loadAlert } from './get_rule'; +import { loadRule } from './get_rule'; import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); -describe('loadAlert', () => { +describe('loadRule', () => { test('should call get API with base parameters', async () => { - const alertId = `${uuid.v4()}/`; - const alertIdEncoded = encodeURIComponent(alertId); + const ruleId = `${uuid.v4()}/`; + const ruleIdEncoded = encodeURIComponent(ruleId); const resolvedValue = { id: '1/', params: { @@ -56,7 +56,7 @@ describe('loadAlert', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - expect(await loadAlert({ http, alertId })).toEqual({ + expect(await loadRule({ http, ruleId })).toEqual({ id: '1/', params: { aggType: 'count', @@ -75,7 +75,6 @@ describe('loadAlert', () => { name: 'dfsdfdsf', enabled: true, throttle: '1h', - alertTypeId: '.index-threshold', createdBy: 'elastic', updatedBy: 'elastic', createdAt: '2021-04-01T20:29:18.652Z', @@ -84,6 +83,7 @@ describe('loadAlert', () => { notifyWhen: 'onThrottleInterval', muteAll: false, mutedInstanceIds: [], + ruleTypeId: '.index-threshold', scheduledTaskId: '1', executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, actions: [ @@ -95,6 +95,6 @@ describe('loadAlert', () => { }, ], }); - expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertIdEncoded}`); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleIdEncoded}`); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.ts similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.ts index c89fd9f90e0a39..8604de3cc8bcf1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_rule.ts @@ -8,17 +8,17 @@ import { HttpSetup } from 'kibana/public'; import { AsApiContract } from '../../../../../actions/common'; import { Rule } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { transformAlert } from './common_transformations'; +import { transformRule } from './common_transformations'; -export async function loadAlert({ +export async function loadRule({ http, - alertId, + ruleId, }: { http: HttpSetup; - alertId: string; + ruleId: string; }): Promise { const res = await http.get>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(alertId)}` + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}` ); - return transformAlert(res); + return transformRule(res); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/health.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/health.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/health.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/health.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts new file mode 100644 index 00000000000000..fff1cef678b022 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { alertingFrameworkHealth } from './health'; +export { mapFiltersToKql } from './map_filters_to_kql'; +export { loadRuleAggregations } from './aggregate'; +export { createRule } from './create'; +export { deleteRules } from './delete'; +export { disableRule, disableRules } from './disable'; +export { enableRule, enableRules } from './enable'; +export { loadRule } from './get_rule'; +export { loadRuleSummary } from './rule_summary'; +export { muteAlertInstance } from './mute_alert'; +export { muteRule, muteRules } from './mute'; +export { loadRuleTypes } from './rule_types'; +export { loadRules } from './rules'; +export { loadRuleState } from './state'; +export { unmuteAlertInstance } from './unmute_alert'; +export { unmuteRule, unmuteRules } from './unmute'; +export { updateRule } from './update'; +export { resolveRule } from './resolve_rule'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts similarity index 88% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index 4e5e2a412dad64..e1dd14a7a9fdec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -32,10 +32,10 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle alertStatusesFilter', () => { + test('should handle ruleStatusesFilter', () => { expect( mapFiltersToKql({ - alertStatusesFilter: ['alert', 'statuses', 'filter'], + ruleStatusesFilter: ['alert', 'statuses', 'filter'], }) ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); }); @@ -52,12 +52,12 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { + test('should handle typesFilter, actionTypesFilter and ruleStatusesFilter', () => { expect( mapFiltersToKql({ typesFilter: ['type', 'filter'], actionTypesFilter: ['action', 'types', 'filter'], - alertStatusesFilter: ['alert', 'statuses', 'filter'], + ruleStatusesFilter: ['alert', 'statuses', 'filter'], }) ).toEqual([ 'alert.attributes.alertTypeId:(type or filter)', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index 4c30e960034bfc..d7b22a7a4aee48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -8,11 +8,11 @@ export const mapFiltersToKql = ({ typesFilter, actionTypesFilter, - alertStatusesFilter, + ruleStatusesFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; + ruleStatusesFilter?: string[]; }): string[] => { const filters = []; if (typesFilter && typesFilter.length) { @@ -29,8 +29,8 @@ export const mapFiltersToKql = ({ ].join('') ); } - if (alertStatusesFilter && alertStatusesFilter.length) { - filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); + if (ruleStatusesFilter && ruleStatusesFilter.length) { + filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`); } return filters; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.test.ts similarity index 83% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.test.ts index 804096dbafac89..f89dfdd41ebe6c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.test.ts @@ -6,14 +6,14 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { muteAlert, muteAlerts } from './mute'; +import { muteRule, muteRules } from './mute'; const http = httpServiceMock.createStartContract(); beforeEach(() => jest.resetAllMocks()); -describe('muteAlert', () => { +describe('muteRule', () => { test('should call mute alert API', async () => { - const result = await muteAlert({ http, id: '1/' }); + const result = await muteRule({ http, id: '1/' }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ @@ -25,10 +25,10 @@ describe('muteAlert', () => { }); }); -describe('muteAlerts', () => { +describe('muteRules', () => { test('should call mute alert API per alert', async () => { const ids = ['1', '2', '/']; - const result = await muteAlerts({ http, ids }); + const result = await muteRules({ http, ids }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.ts similarity index 63% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.ts index 888cdfa92c8f5e..81bbcf2e2b270f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute.ts @@ -7,10 +7,10 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ALERTING_API_PATH } from '../../constants'; -export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { +export async function muteRule({ id, http }: { id: string; http: HttpSetup }): Promise { await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_mute_all`); } -export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map((id) => muteAlert({ http, id }))); +export async function muteRules({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { + await Promise.all(ids.map((id) => muteRule({ http, id }))); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute_alert.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute_alert.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute_alert.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/mute_alert.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/resolve_rule.test.ts similarity index 98% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/resolve_rule.test.ts index 14b64f56f31ff9..fe9ce240b50e5c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/resolve_rule.test.ts @@ -77,7 +77,6 @@ describe('resolveRule', () => { name: 'dfsdfdsf', enabled: true, throttle: '1h', - alertTypeId: '.index-threshold', createdBy: 'elastic', updatedBy: 'elastic', createdAt: '2021-04-01T20:29:18.652Z', @@ -86,6 +85,7 @@ describe('resolveRule', () => { notifyWhen: 'onThrottleInterval', muteAll: false, mutedInstanceIds: [], + ruleTypeId: '.index-threshold', scheduledTaskId: '1', executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, actions: [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/resolve_rule.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/resolve_rule.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts similarity index 84% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts index 7a1fdbe53c7c54..ed81bae9777b61 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts @@ -6,14 +6,14 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { AlertSummary } from '../../../../../alerting/common'; -import { loadAlertSummary } from './alert_summary'; +import { RuleSummary } from '../../../types'; +import { loadRuleSummary } from './rule_summary'; const http = httpServiceMock.createStartContract(); -describe('loadAlertSummary', () => { - test('should call alert summary API', async () => { - const resolvedValue: AlertSummary = { +describe('loadRuleSummary', () => { + test('should call rule summary API', async () => { + const resolvedValue: RuleSummary = { alerts: {}, consumer: 'alerts', enabled: true, @@ -55,7 +55,7 @@ describe('loadAlertSummary', () => { }, }); - const result = await loadAlertSummary({ http, ruleId: 'te/st' }); + const result = await loadRuleSummary({ http, ruleId: 'te/st' }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.ts similarity index 84% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.ts index 55ba674dbd7f51..0ddc58602bd5e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.ts @@ -6,7 +6,7 @@ */ import { HttpSetup } from 'kibana/public'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { AlertSummary, ExecutionDuration } from '../../../types'; +import { RuleSummary, ExecutionDuration } from '../../../types'; import { RewriteRequestCase, AsApiContract } from '../../../../../actions/common'; const transformExecutionDuration: RewriteRequestCase = ({ @@ -17,7 +17,7 @@ const transformExecutionDuration: RewriteRequestCase = ({ ...rest, }); -const rewriteBodyRes: RewriteRequestCase = ({ +const rewriteBodyRes: RewriteRequestCase = ({ rule_type_id: ruleTypeId, mute_all: muteAll, status_start_date: statusStartDate, @@ -37,7 +37,7 @@ const rewriteBodyRes: RewriteRequestCase = ({ executionDuration: executionDuration ? transformExecutionDuration(executionDuration) : undefined, }); -export async function loadAlertSummary({ +export async function loadRuleSummary({ http, ruleId, numberOfExecutions, @@ -45,8 +45,8 @@ export async function loadAlertSummary({ http: HttpSetup; ruleId: string; numberOfExecutions?: number; -}): Promise { - const res = await http.get>( +}): Promise { + const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_alert_summary`, { query: { number_of_executions: numberOfExecutions }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.test.ts similarity index 91% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.test.ts index 49f50858b69835..cd14c9f7079601 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.test.ts @@ -7,12 +7,12 @@ import { RuleType } from '../../../types'; import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { loadAlertTypes } from './rule_types'; +import { loadRuleTypes } from './rule_types'; import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; const http = httpServiceMock.createStartContract(); -describe('loadAlertTypes', () => { +describe('loadRuleTypes', () => { test('should call get alert types API', async () => { const resolvedValue: RuleType[] = [ { @@ -34,7 +34,7 @@ describe('loadAlertTypes', () => { ]; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlertTypes({ http }); + const result = await loadRuleTypes({ http }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.ts similarity index 94% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.ts index 63207dd35dfaca..2b96deaa0dfb10 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_types.ts @@ -37,7 +37,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ ...rest, }); -export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { +export async function loadRuleTypes({ http }: { http: HttpSetup }): Promise { const res = await http.get>>>( `${BASE_ALERTING_API_PATH}/rule_types` ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts similarity index 93% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 6453876938b4e3..2b70955ded4c3e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -5,11 +5,11 @@ * 2.0. */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { loadAlerts } from './rules'; +import { loadRules } from './rules'; const http = httpServiceMock.createStartContract(); -describe('loadAlerts', () => { +describe('loadRules', () => { beforeEach(() => jest.resetAllMocks()); test('should call find API with base parameters', async () => { @@ -21,7 +21,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); + const result = await loadRules({ http, page: { index: 0, size: 10 } }); expect(result).toEqual({ page: 1, perPage: 10, @@ -56,7 +56,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); + const result = await loadRules({ http, searchText: 'apples', page: { index: 0, size: 10 } }); expect(result).toEqual({ page: 1, perPage: 10, @@ -91,7 +91,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ + const result = await loadRules({ http, searchText: 'foo', page: { index: 0, size: 10 }, @@ -130,7 +130,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ + const result = await loadRules({ http, typesFilter: ['foo', 'bar'], page: { index: 0, size: 10 }, @@ -169,7 +169,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ + const result = await loadRules({ http, searchText: 'baz', typesFilter: ['foo', 'bar'], @@ -209,7 +209,7 @@ describe('loadAlerts', () => { }; http.get.mockResolvedValueOnce(resolvedValue); - const result = await loadAlerts({ + const result = await loadRules({ http, searchText: 'apples, foo, baz', typesFilter: ['foo', 'bar'], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts similarity index 88% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index 2f75ca2bbd0ba8..97c432a4803550 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -9,19 +9,19 @@ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { Rule, Pagination, Sorting } from '../../../types'; import { AsApiContract } from '../../../../../actions/common'; import { mapFiltersToKql } from './map_filters_to_kql'; -import { transformAlert } from './common_transformations'; +import { transformRule } from './common_transformations'; const rewriteResponseRes = (results: Array>): Rule[] => { - return results.map((item) => transformAlert(item)); + return results.map((item) => transformRule(item)); }; -export async function loadAlerts({ +export async function loadRules({ http, page, searchText, typesFilter, actionTypesFilter, - alertStatusesFilter, + ruleStatusesFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -29,7 +29,7 @@ export async function loadAlerts({ searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; + ruleStatusesFilter?: string[]; sort?: Sorting; }): Promise<{ page: number; @@ -37,7 +37,7 @@ export async function loadAlerts({ total: number; data: Rule[]; }> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter }); const res = await http.get< AsApiContract<{ page: number; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.test.ts similarity index 81% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.test.ts index ae27352be0b906..db22fa4d2d8963 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.test.ts @@ -6,16 +6,16 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { loadAlertState } from './state'; +import { loadRuleState } from './state'; import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); -describe('loadAlertState', () => { +describe('loadRuleState', () => { beforeEach(() => jest.resetAllMocks()); test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); + const ruleId = uuid.v4(); const resolvedValue = { alertTypeState: { some: 'value', @@ -35,12 +35,12 @@ describe('loadAlertState', () => { }, }); - expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + expect(await loadRuleState({ http, ruleId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleId}/state`); }); - test('should parse AlertInstances', async () => { - const alertId = uuid.v4(); + test('should parse RuleInstances', async () => { + const ruleId = uuid.v4(); const resolvedValue = { alertTypeState: { some: 'value', @@ -74,7 +74,7 @@ describe('loadAlertState', () => { }, }); - expect(await loadAlertState({ http, alertId })).toEqual({ + expect(await loadRuleState({ http, ruleId })).toEqual({ ...resolvedValue, alertInstances: { first_instance: { @@ -88,14 +88,14 @@ describe('loadAlertState', () => { }, }, }); - expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleId}/state`); }); test('should handle empty response from api', async () => { - const alertId = uuid.v4(); + const ruleId = uuid.v4(); http.get.mockResolvedValueOnce(''); - expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + expect(await loadRuleState({ http, ruleId })).toEqual({}); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleId}/state`); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.ts similarity index 87% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.ts index b0b321159bf1c9..35fd63d92d25b0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/state.ts @@ -26,23 +26,23 @@ const rewriteBodyRes: RewriteRequestCase = ({ }); type EmptyHttpResponse = ''; -export async function loadAlertState({ +export async function loadRuleState({ http, - alertId, + ruleId, }: { http: HttpSetup; - alertId: string; + ruleId: string; }): Promise { return await http .get | EmptyHttpResponse>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/state` + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${ruleId}/state` ) .then((state) => (state ? rewriteBodyRes(state) : {})) .then((state: RuleTaskState) => { return pipe( ruleStateSchema.decode(state), fold((e: Errors) => { - throw new Error(`Rule "${alertId}" has invalid state`); + throw new Error(`Rule "${ruleId}" has invalid state`); }, identity) ); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.test.ts similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.test.ts index dfaceffcf8f00a..fe55c2e2656fb0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.test.ts @@ -6,15 +6,15 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { unmuteAlert, unmuteAlerts } from './unmute'; +import { unmuteRule, unmuteRules } from './unmute'; const http = httpServiceMock.createStartContract(); beforeEach(() => jest.resetAllMocks()); -describe('unmuteAlerts', () => { - test('should call unmute alert API per alert', async () => { +describe('unmuteRules', () => { + test('should call unmute rule API per rule', async () => { const ids = ['1', '2', '/']; - const result = await unmuteAlerts({ http, ids }); + const result = await unmuteRules({ http, ids }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ @@ -32,9 +32,9 @@ describe('unmuteAlerts', () => { }); }); -describe('unmuteAlert', () => { - test('should call unmute alert API', async () => { - const result = await unmuteAlert({ http, id: '1/' }); +describe('unmuteRule', () => { + test('should call unmute rule API', async () => { + const result = await unmuteRule({ http, id: '1/' }); expect(result).toEqual(undefined); expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts index bd2139f0526451..951038159ca813 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts @@ -7,16 +7,16 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ALERTING_API_PATH } from '../../constants'; -export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { +export async function unmuteRule({ id, http }: { id: string; http: HttpSetup }): Promise { await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_unmute_all`); } -export async function unmuteAlerts({ +export async function unmuteRules({ ids, http, }: { ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); + await Promise.all(ids.map((id) => unmuteRule({ id, http }))); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute_alert.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute_alert.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute_alert.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute_alert.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts index bc6e7aedd122b5..911245de63f673 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts @@ -5,16 +5,15 @@ * 2.0. */ -import { Rule } from '../../../types'; +import { Rule, RuleNotifyWhenType } from '../../../types'; import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { updateAlert } from './update'; -import { AlertNotifyWhenType } from '../../../../../alerting/common'; +import { updateRule } from './update'; const http = httpServiceMock.createStartContract(); -describe('updateAlert', () => { - test('should call alert update API', async () => { - const alertToUpdate = { +describe('updateRule', () => { + test('should call rule update API', async () => { + const ruleToUpdate = { throttle: '1m', consumer: 'alerts', name: 'test', @@ -28,13 +27,13 @@ describe('updateAlert', () => { updatedAt: new Date('1970-01-01T00:00:00.000Z'), apiKey: null, apiKeyOwner: null, - notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, + notifyWhen: 'onThrottleInterval' as RuleNotifyWhenType, }; const resolvedValue: Rule = { - ...alertToUpdate, + ...ruleToUpdate, id: '12/3', enabled: true, - alertTypeId: 'test', + ruleTypeId: 'test', createdBy: null, updatedBy: null, muteAll: false, @@ -46,7 +45,7 @@ describe('updateAlert', () => { }; http.put.mockResolvedValueOnce(resolvedValue); - const result = await updateAlert({ http, id: '12/3', alert: alertToUpdate }); + const result = await updateRule({ http, id: '12/3', rule: ruleToUpdate }); expect(result).toEqual(resolvedValue); expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts similarity index 71% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts index 4867d33ab2dd17..a3b9d081c20d30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts @@ -7,15 +7,15 @@ import { HttpSetup } from 'kibana/public'; import { pick } from 'lodash'; import { BASE_ALERTING_API_PATH } from '../../constants'; -import { Rule, AlertUpdates } from '../../../types'; +import { Rule, RuleUpdates } from '../../../types'; import { RewriteResponseCase, AsApiContract } from '../../../../../actions/common'; -import { transformAlert } from './common_transformations'; +import { transformRule } from './common_transformations'; -type AlertUpdatesBody = Pick< - AlertUpdates, +type RuleUpdatesBody = Pick< + RuleUpdates, 'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen' >; -const rewriteBodyRequest: RewriteResponseCase = ({ +const rewriteBodyRequest: RewriteResponseCase = ({ notifyWhen, actions, ...res @@ -29,14 +29,14 @@ const rewriteBodyRequest: RewriteResponseCase = ({ })), }); -export async function updateAlert({ +export async function updateRule({ http, - alert, + rule, id, }: { http: HttpSetup; - alert: Pick< - AlertUpdates, + rule: Pick< + RuleUpdates, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' >; id: string; @@ -46,10 +46,10 @@ export async function updateAlert({ { body: JSON.stringify( rewriteBodyRequest( - pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) + pick(rule, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) ) ), } ); - return transformAlert(res); + return transformRule(res); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts index faa32204ba66ed..e9ec012707c7be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts @@ -6,18 +6,18 @@ */ import { RuleTypeModel } from '../../types'; -import { alertTypeGroupCompare, alertTypeCompare } from './alert_type_compare'; -import { IsEnabledResult, IsDisabledResult } from './check_alert_type_enabled'; +import { ruleTypeGroupCompare, ruleTypeCompare } from './rule_type_compare'; +import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; -test('should sort groups by containing enabled alert types first and then by name', async () => { - const alertTypes: Array< +test('should sort groups by containing enabled rule types first and then by name', async () => { + const ruleTypes: Array< [ string, Array<{ id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }> ] > = [ @@ -28,8 +28,8 @@ test('should sort groups by containing enabled alert types first and then by nam id: '1', name: 'test2', checkEnabledResult: { isEnabled: false, message: 'gold license' }, - alertTypeItem: { - id: 'my-alert-type', + ruleTypeItem: { + id: 'my-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -49,8 +49,8 @@ test('should sort groups by containing enabled alert types first and then by nam id: '2', name: 'abc', checkEnabledResult: { isEnabled: false, message: 'platinum license' }, - alertTypeItem: { - id: 'my-alert-type', + ruleTypeItem: { + id: 'my-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -65,8 +65,8 @@ test('should sort groups by containing enabled alert types first and then by nam id: '3', name: 'cdf', checkEnabledResult: { isEnabled: true }, - alertTypeItem: { - id: 'disabled-alert-type', + ruleTypeItem: { + id: 'disabled-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -86,8 +86,8 @@ test('should sort groups by containing enabled alert types first and then by nam id: '4', name: 'cde', checkEnabledResult: { isEnabled: true }, - alertTypeItem: { - id: 'my-alert-type', + ruleTypeItem: { + id: 'my-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -107,25 +107,25 @@ test('should sort groups by containing enabled alert types first and then by nam groups.set('bcd', 'BCD'); groups.set('cde', 'CDE'); - const result = [...alertTypes].sort((right, left) => alertTypeGroupCompare(right, left, groups)); - expect(result[0]).toEqual(alertTypes[1]); - expect(result[1]).toEqual(alertTypes[2]); - expect(result[2]).toEqual(alertTypes[0]); + const result = [...ruleTypes].sort((right, left) => ruleTypeGroupCompare(right, left, groups)); + expect(result[0]).toEqual(ruleTypes[1]); + expect(result[1]).toEqual(ruleTypes[2]); + expect(result[2]).toEqual(ruleTypes[0]); }); -test('should sort alert types by enabled first and then by name', async () => { - const alertTypes: Array<{ +test('should sort rule types by enabled first and then by name', async () => { + const ruleTypes: Array<{ id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }> = [ { id: '1', name: 'bcd', checkEnabledResult: { isEnabled: false, message: 'gold license' }, - alertTypeItem: { - id: 'my-alert-type', + ruleTypeItem: { + id: 'my-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -140,8 +140,8 @@ test('should sort alert types by enabled first and then by name', async () => { id: '2', name: 'abc', checkEnabledResult: { isEnabled: false, message: 'platinum license' }, - alertTypeItem: { - id: 'my-alert-type', + ruleTypeItem: { + id: 'my-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -156,8 +156,8 @@ test('should sort alert types by enabled first and then by name', async () => { id: '3', name: 'cdf', checkEnabledResult: { isEnabled: true }, - alertTypeItem: { - id: 'disabled-alert-type', + ruleTypeItem: { + id: 'disabled-rule-type', iconClass: 'test', description: 'Alert when testing', documentationUrl: 'https://localhost.local/docs', @@ -169,8 +169,8 @@ test('should sort alert types by enabled first and then by name', async () => { }, }, ]; - const result = [...alertTypes].sort(alertTypeCompare); - expect(result[0]).toEqual(alertTypes[2]); - expect(result[1]).toEqual(alertTypes[1]); - expect(result[2]).toEqual(alertTypes[0]); + const result = [...ruleTypes].sort(ruleTypeCompare); + expect(result[0]).toEqual(ruleTypes[2]); + expect(result[1]).toEqual(ruleTypes[1]); + expect(result[2]).toEqual(ruleTypes[0]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts similarity index 63% rename from x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts rename to x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts index d05fb400b5e56d..c04574f7961e08 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts @@ -6,16 +6,16 @@ */ import { RuleTypeModel } from '../../types'; -import { IsEnabledResult, IsDisabledResult } from './check_alert_type_enabled'; +import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; -export function alertTypeGroupCompare( +export function ruleTypeGroupCompare( left: [ string, Array<{ id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }> ], right: [ @@ -24,28 +24,28 @@ export function alertTypeGroupCompare( id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }> ], groupNames: Map | undefined ) { const groupNameA = left[0]; const groupNameB = right[0]; - const leftAlertTypesList = left[1]; - const rightAlertTypesList = right[1]; + const leftRuleTypesList = left[1]; + const rightRuleTypesList = right[1]; - const hasEnabledAlertTypeInListLeft = - leftAlertTypesList.find((alertTypeItem) => alertTypeItem.checkEnabledResult.isEnabled) !== + const hasEnabledRuleTypeInListLeft = + leftRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== undefined; - const hasEnabledAlertTypeInListRight = - rightAlertTypesList.find((alertTypeItem) => alertTypeItem.checkEnabledResult.isEnabled) !== + const hasEnabledRuleTypeInListRight = + rightRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== undefined; - if (hasEnabledAlertTypeInListLeft && !hasEnabledAlertTypeInListRight) { + if (hasEnabledRuleTypeInListLeft && !hasEnabledRuleTypeInListRight) { return -1; } - if (!hasEnabledAlertTypeInListLeft && hasEnabledAlertTypeInListRight) { + if (!hasEnabledRuleTypeInListLeft && hasEnabledRuleTypeInListRight) { return 1; } @@ -54,18 +54,18 @@ export function alertTypeGroupCompare( : groupNameA.localeCompare(groupNameB); } -export function alertTypeCompare( +export function ruleTypeCompare( a: { id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }, b: { id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; } ) { if (a.checkEnabledResult.isEnabled === true && b.checkEnabledResult.isEnabled === false) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts index c6524c9d05d565..2e8c8b00ced42a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts @@ -10,7 +10,7 @@ import { throwIfIsntContained, isValidUrl, getConnectorWithInvalidatedFields, - getAlertWithInvalidatedFields, + getRuleWithInvalidatedFields, } from './value_validators'; import uuid from 'uuid'; import { Rule, IErrorObject, UserConfiguredActionConnector } from '../../types'; @@ -155,9 +155,9 @@ describe('getConnectorWithInvalidatedFields', () => { }); }); -describe('getAlertWithInvalidatedFields', () => { - test('sets to null all fields that are required but undefined in alert', () => { - const alert: Rule = { +describe('getRuleWithInvalidatedFields', () => { + test('sets to null all fields that are required but undefined in rule', () => { + const rule: Rule = { params: {}, consumer: 'test', schedule: { @@ -171,17 +171,17 @@ describe('getAlertWithInvalidatedFields', () => { } as any; const baseAlertErrors = { name: ['Name is required.'], - alertTypeId: ['Rule type is required.'], + ruleTypeId: ['Rule type is required.'], }; const actionsErrors: IErrorObject[] = []; const paramsErrors = {}; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect(alert.name).toBeNull(); - expect(alert.alertTypeId).toBeNull(); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect(rule.name).toBeNull(); + expect(rule.ruleTypeId).toBeNull(); }); - test('does not set to null any fields that are required and defined but invalid in alert', () => { - const alert: Rule = { + test('does not set to null any fields that are required and defined but invalid in rule', () => { + const rule: Rule = { name: 'test', id: '123', params: {}, @@ -198,14 +198,14 @@ describe('getAlertWithInvalidatedFields', () => { const baseAlertErrors = { consumer: ['Consumer is invalid.'] }; const actionsErrors: IErrorObject[] = []; const paramsErrors = {}; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect(alert.consumer).toEqual('@@@@'); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect(rule.consumer).toEqual('@@@@'); }); - test('set to null all fields that are required but undefined in alert params', () => { - const alert: Rule = { + test('set to null all fields that are required but undefined in rule params', () => { + const rule: Rule = { name: 'test', - alertTypeId: '.threshold', + ruleTypeId: '.threshold', id: '123', params: {}, consumer: 'test', @@ -232,15 +232,15 @@ describe('getAlertWithInvalidatedFields', () => { const baseAlertErrors = {}; const actionsErrors: IErrorObject[] = []; const paramsErrors = { index: ['Index is required.'], timeField: ['Time field is required.'] }; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect(alert.params.index).toBeNull(); - expect(alert.params.timeField).toBeNull(); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect(rule.params.index).toBeNull(); + expect(rule.params.timeField).toBeNull(); }); - test('does not set to null any fields that are required and defined but invalid in alert params', () => { - const alert: Rule = { + test('does not set to null any fields that are required and defined but invalid in rule params', () => { + const rule: Rule = { name: 'test', - alertTypeId: '.threshold', + ruleTypeId: '.threshold', id: '123', params: { aggField: 'foo', @@ -273,15 +273,15 @@ describe('getAlertWithInvalidatedFields', () => { aggField: ['Aggregation field is invalid.'], termSize: ['Term size is invalid.'], }; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect(alert.params.aggField).toEqual('foo'); - expect(alert.params.termSize).toEqual('big'); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect(rule.params.aggField).toEqual('foo'); + expect(rule.params.termSize).toEqual('big'); }); - test('set to null all fields that are required but undefined in alert actions', () => { - const alert: Rule = { + test('set to null all fields that are required but undefined in rule actions', () => { + const rule: Rule = { name: 'test', - alertTypeId: '.threshold', + ruleTypeId: '.threshold', id: '123', params: {}, consumer: 'test', @@ -319,14 +319,14 @@ describe('getAlertWithInvalidatedFields', () => { const baseAlertErrors = {}; const actionsErrors = [{ 'incident.field.name': ['Name is required.'] }]; const paramsErrors = {}; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect((alert.actions[0].params as any).incident.field.name).toBeNull(); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect((rule.actions[0].params as any).incident.field.name).toBeNull(); }); - test('validates multiple alert actions with the same connector id', () => { - const alert: Rule = { + test('validates multiple rule actions with the same connector id', () => { + const rule: Rule = { name: 'test', - alertTypeId: '.threshold', + ruleTypeId: '.threshold', id: '123', params: {}, consumer: 'test', @@ -379,8 +379,8 @@ describe('getAlertWithInvalidatedFields', () => { { 'incident.field.name': ['Name is invalid.'] }, ]; const paramsErrors = {}; - getAlertWithInvalidatedFields(alert, paramsErrors, baseAlertErrors, actionsErrors); - expect((alert.actions[0].params as any).incident.field.name).toBeNull(); - expect((alert.actions[1].params as any).incident.field.name).toEqual('myIncident'); + getRuleWithInvalidatedFields(rule, paramsErrors, baseAlertErrors, actionsErrors); + expect((rule.actions[0].params as any).incident.field.name).toBeNull(); + expect((rule.actions[1].params as any).incident.field.name).toEqual('myIncident'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts index 161776dfe0a3ed..e4e27291f972a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts @@ -70,24 +70,24 @@ export function getConnectorWithInvalidatedFields( return connector; } -export function getAlertWithInvalidatedFields( - alert: Rule, +export function getRuleWithInvalidatedFields( + rule: Rule, paramsErrors: IErrorObject, baseAlertErrors: IErrorObject, actionsErrors: IErrorObject[] ) { Object.keys(paramsErrors).forEach((errorKey) => { - if (paramsErrors[errorKey].length >= 1 && get(alert.params, errorKey) === undefined) { - set(alert.params, errorKey, null); + if (paramsErrors[errorKey].length >= 1 && get(rule.params, errorKey) === undefined) { + set(rule.params, errorKey, null); } }); Object.keys(baseAlertErrors).forEach((errorKey) => { - if (baseAlertErrors[errorKey].length >= 1 && get(alert, errorKey) === undefined) { - set(alert, errorKey, null); + if (baseAlertErrors[errorKey].length >= 1 && get(rule, errorKey) === undefined) { + set(rule, errorKey, null); } }); actionsErrors.forEach((error: IErrorObject, index: number) => { - const actionToValidate = alert.actions.length > index ? alert.actions[index] : null; + const actionToValidate = rule.actions.length > index ? rule.actions[index] : null; if (actionToValidate) { Object.keys(error).forEach((errorKey) => { if (error[errorKey].length >= 1 && get(actionToValidate!.params, errorKey) === undefined) { @@ -96,5 +96,5 @@ export function getAlertWithInvalidatedFields( }); } }); - return alert; + return rule; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index ace563aa96b9ec..97945fd8752c11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -14,7 +14,7 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Rule, - AlertAction, + RuleAction, ConnectorValidationResult, GenericValidationResult, } from '../../../types'; @@ -193,7 +193,7 @@ describe('action_form', () => { const useKibanaMock = useKibana as jest.Mocked; describe('action_form in alert', () => { - async function setup(customActions?: AlertAction[], customRecoveredActionGroup?: string) { + async function setup(customActions?: RuleAction[], customRecoveredActionGroup?: string) { const actionTypeRegistry = actionTypeRegistryMock.create(); const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); @@ -283,7 +283,7 @@ describe('action_form', () => { setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; }} - setActions={(_updatedActions: AlertAction[]) => {}} + setActions={(_updatedActions: RuleAction[]) => {}} setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index f25827fb4ba993..7c1af79c056265 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -22,7 +22,7 @@ import { import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; import { ActionTypeModel, - AlertAction, + RuleAction, ActionTypeIndex, ActionConnector, ActionType, @@ -48,13 +48,13 @@ export interface ActionGroupWithMessageVariables extends ActionGroup { } export interface ActionAccordionFormProps { - actions: AlertAction[]; + actions: RuleAction[]; defaultActionGroupId: string; actionGroups?: ActionGroupWithMessageVariables[]; defaultActionMessage?: string; setActionIdByIndex: (id: string, index: number) => void; setActionGroupIdByIndex?: (group: string, index: number) => void; - setActions: (actions: AlertAction[]) => void; + setActions: (actions: RuleAction[]) => void; setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void; actionTypes?: ActionType[]; messageVariables?: ActionVariables; @@ -307,7 +307,7 @@ export const ActionForm = ({ {actionTypesIndex && - actions.map((actionItem: AlertAction, index: number) => { + actions.map((actionItem: RuleAction, index: number) => { const actionConnector = connectors.find((field) => field.id === actionItem.id); // connectors doesn't exists if (!actionConnector) { @@ -322,11 +322,11 @@ export const ActionForm = ({ connectors={connectors} onDeleteConnector={() => { const updatedActions = actions.filter( - (_item: AlertAction, i: number) => i !== index + (_item: RuleAction, i: number) => i !== index ); setActions(updatedActions); setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id) + updatedActions.filter((item: RuleAction) => item.id !== actionItem.id) .length === 0 ); setActiveActionItem(undefined); @@ -335,7 +335,7 @@ export const ActionForm = ({ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: actions - .map((item: AlertAction, idx: number) => + .map((item: RuleAction, idx: number) => item.id === actionItem.id ? idx : -1 ) .filter((idx: number) => idx >= 0), @@ -375,11 +375,11 @@ export const ActionForm = ({ actionTypeRegistry={actionTypeRegistry} onDeleteAction={() => { const updatedActions = actions.filter( - (_item: AlertAction, i: number) => i !== index + (_item: RuleAction, i: number) => i !== index ); setActions(updatedActions); setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === + updatedActions.filter((item: RuleAction) => item.id !== actionItem.id).length === 0 ); setActiveActionItem(undefined); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index e04b96b6839cf5..484f6698a8b29f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -11,7 +11,7 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionConnector, ActionType, - AlertAction, + RuleAction, ConnectorValidationResult, GenericValidationResult, } from '../../../types'; @@ -138,7 +138,7 @@ describe('action_type_form', () => { function getActionTypeForm( index?: number, actionConnector?: ActionConnector, Record>, - actionItem?: AlertAction, + actionItem?: RuleAction, defaultActionGroupId?: string, connectors?: Array, Record>>, actionTypeIndex?: Record, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 4a27c4c1e6fefc..c817f5895a816d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -28,7 +28,7 @@ import { partition } from 'lodash'; import { ActionVariable, AlertActionParam } from '../../../../../alerting/common'; import { IErrorObject, - AlertAction, + RuleAction, ActionTypeIndex, ActionConnector, ActionVariables, @@ -43,7 +43,7 @@ import { DefaultActionParams } from '../../lib/get_defaults_for_action_params'; import { ConnectorsSelection } from './connectors_selection'; export type ActionTypeFormProps = { - actionItem: AlertAction; + actionItem: RuleAction; actionConnector: ActionConnector; index: number; onAddConnector: () => void; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx index cd274c542c9d5e..558ae873892da2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx @@ -23,7 +23,7 @@ import { EuiButtonEmpty, EuiIconTip, } from '@elastic/eui'; -import { AlertAction, ActionTypeIndex, ActionConnector } from '../../../types'; +import { RuleAction, ActionTypeIndex, ActionConnector } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { useKibana } from '../../../common/lib/kibana'; @@ -32,7 +32,7 @@ import { ConnectorsSelection } from './connectors_selection'; type AddConnectorInFormProps = { actionTypesIndex: ActionTypeIndex; - actionItem: AlertAction; + actionItem: RuleAction; connectors: ActionConnector[]; index: number; onAddConnector: () => void; @@ -155,7 +155,7 @@ export const AddConnectorInline = ({
{ - const onSaveHandler = onSave ?? reloadAlerts; - - const initialAlert: InitialAlert = useMemo(() => { - return { - params: {}, - consumer, - alertTypeId, - schedule: { - interval: DEFAULT_ALERT_INTERVAL, - }, - actions: [], - tags: [], - notifyWhen: 'onActionGroupChange', - ...(initialValues ? initialValues : {}), - }; - }, [alertTypeId, consumer, initialValues]); - - const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { - alert: initialAlert, - }); - const [initialAlertParams, setInitialAlertParams] = useState({}); - const [isSaving, setIsSaving] = useState(false); - const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); - const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); - const [ruleTypeIndex, setRuleTypeIndex] = useState( - props.ruleTypeIndex - ); - const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); - - const setAlert = (value: InitialAlert) => { - dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); - }; - - const setRuleProperty = (key: Key, value: Rule[Key] | null) => { - dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); - }; - - const { - http, - notifications: { toasts }, - application: { capabilities }, - } = useKibana().services; - - const canShowActions = hasShowActionsCapability(capabilities); - - useEffect(() => { - if (alertTypeId) { - setRuleProperty('alertTypeId', alertTypeId); - } - }, [alertTypeId]); - - useEffect(() => { - if (!props.ruleTypeIndex) { - (async () => { - const alertTypes = await loadAlertTypes({ http }); - const index: RuleTypeIndex = new Map(); - for (const alertType of alertTypes) { - index.set(alertType.id, alertType); - } - setRuleTypeIndex(index); - })(); - } - }, [props.ruleTypeIndex, http]); - - useEffect(() => { - if (isEmpty(alert.params) && !isEmpty(initialAlertParams)) { - // alert params are explicitly cleared when the alert type is cleared. - // clear the "initial" params in order to capture the - // default when a new alert type is selected - setInitialAlertParams({}); - } else if (isEmpty(initialAlertParams)) { - // captures the first change to the alert params, - // when consumers set a default value for the alert params - setInitialAlertParams(alert.params); - } - }, [alert.params, initialAlertParams, setInitialAlertParams]); - - const [alertActionsErrors, setAlertActionsErrors] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - (async () => { - setIsLoading(true); - const res = await getAlertActionErrors(alert as Rule, actionTypeRegistry); - setIsLoading(false); - setAlertActionsErrors([...res]); - })(); - }, [alert, actionTypeRegistry]); - - useEffect(() => { - if (alert.alertTypeId && ruleTypeIndex) { - const type = ruleTypeIndex.get(alert.alertTypeId); - if (type?.defaultScheduleInterval && !changedFromDefaultInterval) { - setRuleProperty('schedule', { interval: type.defaultScheduleInterval }); - } - } - }, [alert.alertTypeId, ruleTypeIndex, alert.schedule.interval, changedFromDefaultInterval]); - - useEffect(() => { - if (alert.schedule.interval !== DEFAULT_ALERT_INTERVAL && !changedFromDefaultInterval) { - setChangedFromDefaultInterval(true); - } - }, [alert.schedule.interval, changedFromDefaultInterval]); - - const checkForChangesAndCloseFlyout = () => { - if ( - hasAlertChanged(alert, initialAlert, false) || - haveAlertParamsChanged(alert.params, initialAlertParams) - ) { - setIsConfirmAlertCloseModalOpen(true); - } else { - onClose(AlertFlyoutCloseReason.CANCELED); - } - }; - - const saveAlertAndCloseFlyout = async () => { - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - onClose(AlertFlyoutCloseReason.SAVED); - if (onSaveHandler) { - onSaveHandler(); - } - } - }; - - const alertType = alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null; - - const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( - alert as Rule, - alertType, - alert.alertTypeId ? ruleTypeIndex?.get(alert.alertTypeId) : undefined - ); - - // Confirm before saving if user is able to add actions but hasn't added any to this alert - const shouldConfirmSave = canShowActions && alert.actions?.length === 0; - - async function onSaveAlert(): Promise { - try { - const newAlert = await createAlert({ http, alert: alert as AlertUpdates }); - toasts.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { - defaultMessage: 'Created rule "{ruleName}"', - values: { - ruleName: newAlert.name, - }, - }) - ); - return newAlert; - } catch (errorRes) { - toasts.addDanger( - errorRes.body?.message ?? - i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText', { - defaultMessage: 'Cannot create rule.', - }) - ); - } - } - - return ( - - - - -

- -

-
-
- - - - - - { - setIsSaving(true); - if (isLoading || !isValidAlert(alert, alertErrors, alertActionsErrors)) { - setAlert( - getAlertWithInvalidatedFields( - alert as Rule, - alertParamsErrors, - alertBaseErrors, - alertActionsErrors - ) - ); - setIsSaving(false); - return; - } - if (shouldConfirmSave) { - setIsConfirmAlertSaveModalOpen(true); - } else { - await saveAlertAndCloseFlyout(); - } - }} - onCancel={checkForChangesAndCloseFlyout} - /> - - - {isConfirmAlertSaveModalOpen && ( - { - setIsConfirmAlertSaveModalOpen(false); - await saveAlertAndCloseFlyout(); - }} - onCancel={() => { - setIsSaving(false); - setIsConfirmAlertSaveModalOpen(false); - }} - /> - )} - {isConfirmAlertCloseModalOpen && ( - { - setIsConfirmAlertCloseModalOpen(false); - onClose(AlertFlyoutCloseReason.CANCELED); - }} - onCancel={() => { - setIsConfirmAlertCloseModalOpen(false); - }} - /> - )} -
-
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { AlertAdd as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts deleted file mode 100644 index a581e702b9a47f..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { alertReducer } from './alert_reducer'; -import { Rule } from '../../../types'; - -describe('alert reducer', () => { - let initialAlert: Rule; - beforeAll(() => { - initialAlert = { - params: {}, - consumer: 'alerts', - alertTypeId: null, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - notifyWhen: 'onActionGroupChange', - } as unknown as Rule; - }); - - // setAlert - test('if modified alert was reset to initial', () => { - const alert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setProperty' }, - payload: { - key: 'name', - value: 'new name', - }, - } - ); - expect(alert.alert.name).toBe('new name'); - - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setAlert' }, - payload: { - key: 'alert', - value: initialAlert, - }, - } - ); - expect(updatedAlert.alert.name).toBeUndefined(); - }); - - test('if property name was changed', () => { - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setProperty' }, - payload: { - key: 'name', - value: 'new name', - }, - } - ); - expect(updatedAlert.alert.name).toBe('new name'); - }); - - test('if initial schedule property was updated', () => { - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setScheduleProperty' }, - payload: { - key: 'interval', - value: '10s', - }, - } - ); - expect(updatedAlert.alert.schedule.interval).toBe('10s'); - }); - - test('if alert params property was added and updated', () => { - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setRuleParams' }, - payload: { - key: 'testParam', - value: 'new test params property', - }, - } - ); - expect(updatedAlert.alert.params.testParam).toBe('new test params property'); - - const updatedAlertParamsProperty = alertReducer( - { alert: updatedAlert.alert }, - { - command: { type: 'setRuleParams' }, - payload: { - key: 'testParam', - value: 'test params property updated', - }, - } - ); - expect(updatedAlertParamsProperty.alert.params.testParam).toBe('test params property updated'); - }); - - test('if alert action params property was added and updated', () => { - initialAlert.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Alert', - params: {}, - }); - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setAlertActionParams' }, - payload: { - key: 'testActionParam', - value: 'new test action params property', - index: 0, - }, - } - ); - expect(updatedAlert.alert.actions[0].params.testActionParam).toBe( - 'new test action params property' - ); - - const updatedAlertActionParamsProperty = alertReducer( - { alert: updatedAlert.alert }, - { - command: { type: 'setAlertActionParams' }, - payload: { - key: 'testActionParam', - value: 'test action params property updated', - index: 0, - }, - } - ); - expect(updatedAlertActionParamsProperty.alert.actions[0].params.testActionParam).toBe( - 'test action params property updated' - ); - }); - - test('if the existing alert action params property was set to undefined (when other connector was selected)', () => { - initialAlert.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Alert', - params: { - testActionParam: 'some value', - }, - }); - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setAlertActionParams' }, - payload: { - key: 'testActionParam', - value: undefined, - index: 0, - }, - } - ); - expect(updatedAlert.alert.actions[0].params.testActionParam).toBe(undefined); - }); - - test('if alert action property was updated', () => { - initialAlert.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Alert', - params: {}, - }); - const updatedAlert = alertReducer( - { alert: initialAlert }, - { - command: { type: 'setAlertActionProperty' }, - payload: { - key: 'group', - value: 'Warning', - index: 0, - }, - } - ); - expect(updatedAlert.alert.actions[0].group).toBe('Warning'); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts deleted file mode 100644 index 5099fa61bdb38d..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectAttribute } from 'kibana/public'; -import { isEqual } from 'lodash'; -import { Reducer } from 'react'; -import { AlertActionParam, IntervalSchedule } from '../../../../../alerting/common'; -import { Rule, AlertAction } from '../../../types'; - -export type InitialAlert = Partial & - Pick; - -interface CommandType< - T extends - | 'setAlert' - | 'setProperty' - | 'setScheduleProperty' - | 'setRuleParams' - | 'setAlertActionParams' - | 'setAlertActionProperty' -> { - type: T; -} - -export interface AlertState { - alert: InitialAlert; -} - -interface Payload { - key: Keys; - value: Value; - index?: number; -} - -interface AlertPayload { - key: Key; - value: Rule[Key] | null; - index?: number; -} - -interface AlertActionPayload { - key: Key; - value: AlertAction[Key] | null; - index?: number; -} - -interface AlertSchedulePayload { - key: Key; - value: IntervalSchedule[Key]; - index?: number; -} - -export type AlertReducerAction = - | { - command: CommandType<'setAlert'>; - payload: Payload<'alert', InitialAlert>; - } - | { - command: CommandType<'setProperty'>; - payload: AlertPayload; - } - | { - command: CommandType<'setScheduleProperty'>; - payload: AlertSchedulePayload; - } - | { - command: CommandType<'setRuleParams'>; - payload: Payload; - } - | { - command: CommandType<'setAlertActionParams'>; - payload: Payload; - } - | { - command: CommandType<'setAlertActionProperty'>; - payload: AlertActionPayload; - }; - -export type InitialAlertReducer = Reducer<{ alert: InitialAlert }, AlertReducerAction>; -export type ConcreteAlertReducer = Reducer<{ alert: Rule }, AlertReducerAction>; - -export const alertReducer = ( - state: { alert: AlertPhase }, - action: AlertReducerAction -) => { - const { alert } = state; - - switch (action.command.type) { - case 'setAlert': { - const { key, value } = action.payload as Payload<'alert', AlertPhase>; - if (key === 'alert') { - return { - ...state, - alert: value, - }; - } else { - return state; - } - } - case 'setProperty': { - const { key, value } = action.payload as AlertPayload; - if (isEqual(alert[key], value)) { - return state; - } else { - return { - ...state, - alert: { - ...alert, - [key]: value, - }, - }; - } - } - case 'setScheduleProperty': { - const { key, value } = action.payload as AlertSchedulePayload; - if (alert.schedule && isEqual(alert.schedule[key], value)) { - return state; - } else { - return { - ...state, - alert: { - ...alert, - schedule: { - ...alert.schedule, - [key]: value, - }, - }, - }; - } - } - case 'setRuleParams': { - const { key, value } = action.payload as Payload>; - if (isEqual(alert.params[key], value)) { - return state; - } else { - return { - ...state, - alert: { - ...alert, - params: { - ...alert.params, - [key]: value, - }, - }, - }; - } - } - case 'setAlertActionParams': { - const { key, value, index } = action.payload as Payload< - keyof AlertAction, - SavedObjectAttribute - >; - if ( - index === undefined || - alert.actions[index] == null || - (!!alert.actions[index][key] && isEqual(alert.actions[index][key], value)) - ) { - return state; - } else { - const oldAction = alert.actions.splice(index, 1)[0]; - const updatedAction = { - ...oldAction, - params: { - ...oldAction.params, - [key]: value, - }, - }; - alert.actions.splice(index, 0, updatedAction); - return { - ...state, - alert: { - ...alert, - actions: [...alert.actions], - }, - }; - } - } - case 'setAlertActionProperty': { - const { key, value, index } = action.payload as AlertActionPayload; - if (index === undefined || isEqual(alert.actions[index][key], value)) { - return state; - } else { - const oldAction = alert.actions.splice(index, 1)[0]; - const updatedAction = { - ...oldAction, - [key]: value, - }; - alert.actions.splice(index, 0, updatedAction); - return { - ...state, - alert: { - ...alert, - actions: [...alert.actions], - }, - }; - } - } - } -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.test.ts deleted file mode 100644 index 580c8914c97b39..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { InitialAlert } from './alert_reducer'; -import { hasAlertChanged } from './has_alert_changed'; - -function createAlert(overrides = {}): InitialAlert { - return { - params: {}, - consumer: 'test', - alertTypeId: 'test', - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - notifyWhen: 'onActionGroupChange', - ...overrides, - }; -} - -test('should return false for same alert', () => { - const a = createAlert(); - expect(hasAlertChanged(a, a, true)).toEqual(false); -}); - -test('should return true for different alert', () => { - const a = createAlert(); - const b = createAlert({ alertTypeId: 'differentTest' }); - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare name field', () => { - // name field doesn't exist initially - const a = createAlert(); - // set name to actual value - const b = createAlert({ name: 'myAlert' }); - // set name to different value - const c = createAlert({ name: 'anotherAlert' }); - // set name to various empty/null/undefined states - const d = createAlert({ name: '' }); - const e = createAlert({ name: undefined }); - const f = createAlert({ name: null }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); - expect(hasAlertChanged(a, c, true)).toEqual(true); - expect(hasAlertChanged(a, d, true)).toEqual(false); - expect(hasAlertChanged(a, e, true)).toEqual(false); - expect(hasAlertChanged(a, f, true)).toEqual(false); - - expect(hasAlertChanged(b, c, true)).toEqual(true); - expect(hasAlertChanged(b, d, true)).toEqual(true); - expect(hasAlertChanged(b, e, true)).toEqual(true); - expect(hasAlertChanged(b, f, true)).toEqual(true); - - expect(hasAlertChanged(c, d, true)).toEqual(true); - expect(hasAlertChanged(c, e, true)).toEqual(true); - expect(hasAlertChanged(c, f, true)).toEqual(true); - - expect(hasAlertChanged(d, e, true)).toEqual(false); - expect(hasAlertChanged(d, f, true)).toEqual(false); -}); - -test('should correctly compare alertTypeId field', () => { - const a = createAlert(); - - // set alertTypeId to different value - const b = createAlert({ alertTypeId: 'myAlertId' }); - // set alertTypeId to various empty/null/undefined states - const c = createAlert({ alertTypeId: '' }); - const d = createAlert({ alertTypeId: undefined }); - const e = createAlert({ alertTypeId: null }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); - expect(hasAlertChanged(a, c, true)).toEqual(true); - expect(hasAlertChanged(a, d, true)).toEqual(true); - expect(hasAlertChanged(a, e, true)).toEqual(true); - - expect(hasAlertChanged(b, c, true)).toEqual(true); - expect(hasAlertChanged(b, d, true)).toEqual(true); - expect(hasAlertChanged(b, e, true)).toEqual(true); - - expect(hasAlertChanged(c, d, true)).toEqual(false); - expect(hasAlertChanged(c, e, true)).toEqual(false); - expect(hasAlertChanged(d, e, true)).toEqual(false); -}); - -test('should correctly compare throttle field', () => { - // throttle field doesn't exist initially - const a = createAlert(); - // set throttle to actual value - const b = createAlert({ throttle: '1m' }); - // set throttle to different value - const c = createAlert({ throttle: '1h' }); - // set throttle to various empty/null/undefined states - const d = createAlert({ throttle: '' }); - const e = createAlert({ throttle: undefined }); - const f = createAlert({ throttle: null }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); - expect(hasAlertChanged(a, c, true)).toEqual(true); - expect(hasAlertChanged(a, d, true)).toEqual(false); - expect(hasAlertChanged(a, e, true)).toEqual(false); - expect(hasAlertChanged(a, f, true)).toEqual(false); - - expect(hasAlertChanged(b, c, true)).toEqual(true); - expect(hasAlertChanged(b, d, true)).toEqual(true); - expect(hasAlertChanged(b, e, true)).toEqual(true); - expect(hasAlertChanged(b, f, true)).toEqual(true); - - expect(hasAlertChanged(c, d, true)).toEqual(true); - expect(hasAlertChanged(c, e, true)).toEqual(true); - expect(hasAlertChanged(c, f, true)).toEqual(true); - - expect(hasAlertChanged(d, e, true)).toEqual(false); - expect(hasAlertChanged(d, f, true)).toEqual(false); -}); - -test('should correctly compare tags field', () => { - const a = createAlert(); - const b = createAlert({ tags: ['first'] }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare schedule field', () => { - const a = createAlert(); - const b = createAlert({ schedule: { interval: '3h' } }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare actions field', () => { - const a = createAlert(); - const b = createAlert({ - actions: [{ actionTypeId: 'action', group: 'group', id: 'actionId', params: {} }], - }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); - -test('should skip comparing params field if compareParams=false', () => { - const a = createAlert(); - const b = createAlert({ params: { newParam: 'value' } }); - - expect(hasAlertChanged(a, b, false)).toEqual(false); -}); - -test('should correctly compare params field if compareParams=true', () => { - const a = createAlert(); - const b = createAlert({ params: { newParam: 'value' } }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare notifyWhen field', () => { - const a = createAlert(); - const b = createAlert({ notifyWhen: 'onActiveAlert' }); - - expect(hasAlertChanged(a, b, true)).toEqual(true); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx index f4edf0ad1a9b85..fe8382e224ffda 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx @@ -25,7 +25,7 @@ export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { onClick={() => setIsPopoverOpen(!isPopoverOpen)} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.scss similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.scss diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx similarity index 58% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx index 347ebb227e4452..208fbcde88143d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx @@ -10,207 +10,207 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { AlertTableItem } from '../../../../types'; +import { RuleTableItem } from '../../../../types'; import { - withBulkAlertOperations, + withBulkRuleOperations, ComponentOpts as BulkOperationsComponentOpts, -} from './with_bulk_alert_api_operations'; -import './alert_quick_edit_buttons.scss'; +} from './with_bulk_rule_api_operations'; +import './rule_quick_edit_buttons.scss'; import { useKibana } from '../../../../common/lib/kibana'; export type ComponentOpts = { - selectedItems: AlertTableItem[]; + selectedItems: RuleTableItem[]; onPerformingAction?: () => void; onActionPerformed?: () => void; - setAlertsToDelete: React.Dispatch>; + setRulesToDelete: React.Dispatch>; } & BulkOperationsComponentOpts; -export const AlertQuickEditButtons: React.FunctionComponent = ({ +export const RuleQuickEditButtons: React.FunctionComponent = ({ selectedItems, onPerformingAction = noop, onActionPerformed = noop, - muteAlerts, - unmuteAlerts, - enableAlerts, - disableAlerts, - setAlertsToDelete, + muteRules, + unmuteRules, + enableRules, + disableRules, + setRulesToDelete, }: ComponentOpts) => { const { notifications: { toasts }, } = useKibana().services; - const [isMutingAlerts, setIsMutingAlerts] = useState(false); - const [isUnmutingAlerts, setIsUnmutingAlerts] = useState(false); - const [isEnablingAlerts, setIsEnablingAlerts] = useState(false); - const [isDisablingAlerts, setIsDisablingAlerts] = useState(false); - const [isDeletingAlerts, setIsDeletingAlerts] = useState(false); + const [isMutingRules, setIsMutingRules] = useState(false); + const [isUnmutingRules, setIsUnmutingRules] = useState(false); + const [isEnablingRules, setIsEnablingRules] = useState(false); + const [isDisablingRules, setIsDisablingRules] = useState(false); + const [isDeletingRules, setIsDeletingRules] = useState(false); - const allAlertsMuted = selectedItems.every(isAlertMuted); - const allAlertsDisabled = selectedItems.every(isAlertDisabled); + const allRulesMuted = selectedItems.every(isRuleMuted); + const allRulesDisabled = selectedItems.every(isRuleDisabled); const isPerformingAction = - isMutingAlerts || isUnmutingAlerts || isEnablingAlerts || isDisablingAlerts || isDeletingAlerts; + isMutingRules || isUnmutingRules || isEnablingRules || isDisablingRules || isDeletingRules; - const hasDisabledByLicenseAlertTypes = !!selectedItems.find( + const hasDisabledByLicenseRuleTypes = !!selectedItems.find( (alertItem) => !alertItem.enabledInLicense ); async function onmMuteAllClick() { onPerformingAction(); - setIsMutingAlerts(true); + setIsMutingRules(true); try { - await muteAlerts(selectedItems); + await muteRules(selectedItems); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToMuteRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToMuteRulesMessage', { defaultMessage: 'Failed to mute rule(s)', } ), }); } finally { - setIsMutingAlerts(false); + setIsMutingRules(false); onActionPerformed(); } } async function onUnmuteAllClick() { onPerformingAction(); - setIsUnmutingAlerts(true); + setIsUnmutingRules(true); try { - await unmuteAlerts(selectedItems); + await unmuteRules(selectedItems); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToUnmuteRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToUnmuteRulesMessage', { defaultMessage: 'Failed to unmute rule(s)', } ), }); } finally { - setIsUnmutingAlerts(false); + setIsUnmutingRules(false); onActionPerformed(); } } async function onEnableAllClick() { onPerformingAction(); - setIsEnablingAlerts(true); + setIsEnablingRules(true); try { - await enableAlerts(selectedItems); + await enableRules(selectedItems); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToEnableRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToEnableRulesMessage', { defaultMessage: 'Failed to enable rule(s)', } ), }); } finally { - setIsEnablingAlerts(false); + setIsEnablingRules(false); onActionPerformed(); } } async function onDisableAllClick() { onPerformingAction(); - setIsDisablingAlerts(true); + setIsDisablingRules(true); try { - await disableAlerts(selectedItems); + await disableRules(selectedItems); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDisableRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDisableRulesMessage', { defaultMessage: 'Failed to disable rule(s)', } ), }); } finally { - setIsDisablingAlerts(false); + setIsDisablingRules(false); onActionPerformed(); } } async function deleteSelectedItems() { onPerformingAction(); - setIsDeletingAlerts(true); + setIsDeletingRules(true); try { - setAlertsToDelete(selectedItems.map((selected: any) => selected.id)); + setRulesToDelete(selectedItems.map((selected: any) => selected.id)); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToDeleteRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToDeleteRulesMessage', { defaultMessage: 'Failed to delete rule(s)', } ), }); } finally { - setIsDeletingAlerts(false); + setIsDeletingRules(false); onActionPerformed(); } } return ( - {!allAlertsMuted && ( + {!allRulesMuted && ( )} - {allAlertsMuted && ( + {allRulesMuted && ( )} - {allAlertsDisabled && ( + {allRulesDisabled && ( )} - {!allAlertsDisabled && ( + {!allRulesDisabled && ( @@ -219,7 +219,7 @@ export const AlertQuickEditButtons: React.FunctionComponent = ({ = ({ className="actBulkActionPopover__deleteAll" > @@ -236,13 +236,13 @@ export const AlertQuickEditButtons: React.FunctionComponent = ({ ); }; -export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); +export const RuleQuickEditButtonsWithApi = withBulkRuleOperations(RuleQuickEditButtons); -function isAlertDisabled(alert: AlertTableItem) { +function isRuleDisabled(alert: RuleTableItem) { return alert.enabled === false; } -function isAlertMuted(alert: AlertTableItem) { +function isRuleMuted(alert: RuleTableItem) { return alert.muteAll === true; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx deleted file mode 100644 index d2194d2246944b..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import { shallow, mount } from 'enzyme'; -import uuid from 'uuid'; -import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; -import * as alertApi from '../../../lib/alert_api'; -import { Rule } from '../../../../types'; -import { useKibana } from '../../../../common/lib/kibana'; -jest.mock('../../../../common/lib/kibana'); - -jest.mock('../../../lib/alert_api'); -const useKibanaMock = useKibana as jest.Mocked; - -describe('with_bulk_alert_api_operations', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('extends any component with AlertApi methods', () => { - const ComponentToExtend = (props: ComponentOpts) => { - expect(typeof props.muteAlerts).toEqual('function'); - expect(typeof props.unmuteAlerts).toEqual('function'); - expect(typeof props.enableAlerts).toEqual('function'); - expect(typeof props.disableAlerts).toEqual('function'); - expect(typeof props.deleteAlerts).toEqual('function'); - expect(typeof props.muteAlert).toEqual('function'); - expect(typeof props.unmuteAlert).toEqual('function'); - expect(typeof props.enableAlert).toEqual('function'); - expect(typeof props.disableAlert).toEqual('function'); - expect(typeof props.deleteAlert).toEqual('function'); - expect(typeof props.loadAlert).toEqual('function'); - expect(typeof props.loadAlertTypes).toEqual('function'); - expect(typeof props.resolveRule).toEqual('function'); - return
; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - expect(shallow().type()).toEqual(ComponentToExtend); - }); - - // single alert - it('muteAlert calls the muteAlert api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alert = mockAlert(); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); - expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); - }); - - it('unmuteAlert calls the unmuteAlert api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alert = mockAlert({ muteAll: true }); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); - expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); - }); - - it('enableAlert calls the muteAlerts api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alert = mockAlert({ enabled: false }); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); - expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); - }); - - it('disableAlert calls the disableAlert api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alert = mockAlert(); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); - expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); - }); - - it('deleteAlert calls the deleteAlert api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alert = mockAlert(); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alert.id], http }); - }); - - // bulk alerts - it('muteAlerts calls the muteAlerts api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Rule[] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alerts = [mockAlert(), mockAlert()]; - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); - }); - - it('unmuteAlerts calls the unmuteAlerts api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Rule[] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); - }); - - it('enableAlerts calls the muteAlertss api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Rule[] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alerts = [ - mockAlert({ enabled: false }), - mockAlert({ enabled: true }), - mockAlert({ enabled: false }), - ]; - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); - }); - - it('disableAlerts calls the disableAlerts api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Rule[] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alerts = [mockAlert(), mockAlert()]; - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.disableAlerts).toHaveBeenCalledWith({ - ids: [alerts[0].id, alerts[1].id], - http, - }); - }); - - it('deleteAlerts calls the deleteAlerts api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Rule[] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alerts = [mockAlert(), mockAlert()]; - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); - expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); - }); - - it('loadAlert calls the loadAlert api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ loadAlert, alertId }: ComponentOpts & { alertId: Rule['id'] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const alertId = uuid.v4(); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); - expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); - }); - - it('resolveRule calls the resolveRule api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ resolveRule, ruleId }: ComponentOpts & { ruleId: Rule['id'] }) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const ruleId = uuid.v4(); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.resolveRule).toHaveBeenCalledTimes(1); - expect(alertApi.resolveRule).toHaveBeenCalledWith({ ruleId, http }); - }); - - it('loadAlertTypes calls the loadAlertTypes api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { - return ; - }; - - const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); - const component = mount(); - component.find('button').simulate('click'); - - expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); - expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); - }); -}); - -function mockAlert(overloads: Partial = {}): Rule { - return { - id: uuid.v4(), - enabled: true, - name: `alert-${uuid.v4()}`, - tags: [], - alertTypeId: '.noop', - consumer: 'consumer', - schedule: { interval: '1m' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - ...overloads, - }; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx deleted file mode 100644 index 0308e17ac342c5..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 { - Rule, - RuleType, - RuleTaskState, - AlertSummary, - AlertingFrameworkHealth, - ResolvedRule, -} from '../../../../types'; -import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, - disableAlert, - enableAlert, - muteAlert, - unmuteAlert, - muteAlertInstance, - unmuteAlertInstance, - loadAlert, - loadAlertState, - loadAlertSummary, - loadAlertTypes, - alertingFrameworkHealth, - resolveRule, -} from '../../../lib/alert_api'; -import { useKibana } from '../../../../common/lib/kibana'; - -export interface ComponentOpts { - muteAlerts: (alerts: Rule[]) => Promise; - unmuteAlerts: (alerts: Rule[]) => Promise; - enableAlerts: (alerts: Rule[]) => Promise; - disableAlerts: (alerts: Rule[]) => Promise; - deleteAlerts: (alerts: Rule[]) => Promise<{ - successes: string[]; - errors: string[]; - }>; - muteAlert: (alert: Rule) => Promise; - unmuteAlert: (alert: Rule) => Promise; - muteAlertInstance: (alert: Rule, alertInstanceId: string) => Promise; - unmuteAlertInstance: (alert: Rule, alertInstanceId: string) => Promise; - enableAlert: (alert: Rule) => Promise; - disableAlert: (alert: Rule) => Promise; - deleteAlert: (alert: Rule) => Promise<{ - successes: string[]; - errors: string[]; - }>; - loadAlert: (id: Rule['id']) => Promise; - loadAlertState: (id: Rule['id']) => Promise; - loadAlertSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; - loadAlertTypes: () => Promise; - getHealth: () => Promise; - resolveRule: (id: Rule['id']) => Promise; -} - -export type PropsWithOptionalApiHandlers = Omit & Partial; - -export function withBulkAlertOperations( - WrappedComponent: React.ComponentType -): React.FunctionComponent> { - return (props: PropsWithOptionalApiHandlers) => { - const { http } = useKibana().services; - return ( - - muteAlerts({ - http, - ids: items.filter((item) => !isAlertMuted(item)).map((item) => item.id), - }) - } - unmuteAlerts={async (items: Rule[]) => - unmuteAlerts({ http, ids: items.filter(isAlertMuted).map((item) => item.id) }) - } - enableAlerts={async (items: Rule[]) => - enableAlerts({ http, ids: items.filter(isAlertDisabled).map((item) => item.id) }) - } - disableAlerts={async (items: Rule[]) => - disableAlerts({ - http, - ids: items.filter((item) => !isAlertDisabled(item)).map((item) => item.id), - }) - } - deleteAlerts={async (items: Rule[]) => - deleteAlerts({ http, ids: items.map((item) => item.id) }) - } - muteAlert={async (alert: Rule) => { - if (!isAlertMuted(alert)) { - return await muteAlert({ http, id: alert.id }); - } - }} - unmuteAlert={async (alert: Rule) => { - if (isAlertMuted(alert)) { - return await unmuteAlert({ http, id: alert.id }); - } - }} - muteAlertInstance={async (alert: Rule, instanceId: string) => { - if (!isAlertInstanceMuted(alert, instanceId)) { - return muteAlertInstance({ http, id: alert.id, instanceId }); - } - }} - unmuteAlertInstance={async (alert: Rule, instanceId: string) => { - if (isAlertInstanceMuted(alert, instanceId)) { - return unmuteAlertInstance({ http, id: alert.id, instanceId }); - } - }} - enableAlert={async (alert: Rule) => { - if (isAlertDisabled(alert)) { - return await enableAlert({ http, id: alert.id }); - } - }} - disableAlert={async (alert: Rule) => { - if (!isAlertDisabled(alert)) { - return await disableAlert({ http, id: alert.id }); - } - }} - deleteAlert={async (alert: Rule) => deleteAlerts({ http, ids: [alert.id] })} - loadAlert={async (alertId: Rule['id']) => loadAlert({ http, alertId })} - loadAlertState={async (alertId: Rule['id']) => loadAlertState({ http, alertId })} - loadAlertSummary={async (ruleId: Rule['id'], numberOfExecutions?: number) => - loadAlertSummary({ http, ruleId, numberOfExecutions }) - } - loadAlertTypes={async () => loadAlertTypes({ http })} - resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })} - getHealth={async () => alertingFrameworkHealth({ http })} - /> - ); - }; -} - -function isAlertDisabled(alert: Rule) { - return alert.enabled === false; -} - -function isAlertMuted(alert: Rule) { - return alert.muteAll === true; -} - -function isAlertInstanceMuted(alert: Rule, instanceId: string) { - return alert.mutedInstanceIds.findIndex((muted) => muted === instanceId) >= 0; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx new file mode 100644 index 00000000000000..d0950c0c75fc22 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx @@ -0,0 +1,277 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkRuleOperations, ComponentOpts } from './with_bulk_rule_api_operations'; +import * as ruleApi from '../../../lib/rule_api'; +import { Rule } from '../../../../types'; +import { useKibana } from '../../../../common/lib/kibana'; +jest.mock('../../../../common/lib/kibana'); + +jest.mock('../../../lib/rule_api'); +const useKibanaMock = useKibana as jest.Mocked; + +describe('with_bulk_rule_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with RuleApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteRules).toEqual('function'); + expect(typeof props.unmuteRules).toEqual('function'); + expect(typeof props.enableRules).toEqual('function'); + expect(typeof props.disableRules).toEqual('function'); + expect(typeof props.deleteRules).toEqual('function'); + expect(typeof props.muteRule).toEqual('function'); + expect(typeof props.unmuteRule).toEqual('function'); + expect(typeof props.enableRule).toEqual('function'); + expect(typeof props.disableRule).toEqual('function'); + expect(typeof props.deleteRule).toEqual('function'); + expect(typeof props.loadRule).toEqual('function'); + expect(typeof props.loadRuleTypes).toEqual('function'); + expect(typeof props.resolveRule).toEqual('function'); + return
; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + // single rule + it('muteRule calls the muteRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ muteRule, rule }: ComponentOpts & { rule: Rule }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rule = mockRule(); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.muteRule).toHaveBeenCalledTimes(1); + expect(ruleApi.muteRule).toHaveBeenCalledWith({ id: rule.id, http }); + }); + + it('unmuteRule calls the unmuteRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ unmuteRule, rule }: ComponentOpts & { rule: Rule }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rule = mockRule({ muteAll: true }); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.unmuteRule).toHaveBeenCalledTimes(1); + expect(ruleApi.unmuteRule).toHaveBeenCalledWith({ id: rule.id, http }); + }); + + it('enableRule calls the muteRules api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ enableRule, rule }: ComponentOpts & { rule: Rule }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rule = mockRule({ enabled: false }); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.enableRule).toHaveBeenCalledTimes(1); + expect(ruleApi.enableRule).toHaveBeenCalledWith({ id: rule.id, http }); + }); + + it('disableRule calls the disableRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ disableRule, rule }: ComponentOpts & { rule: Rule }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rule = mockRule(); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.disableRule).toHaveBeenCalledTimes(1); + expect(ruleApi.disableRule).toHaveBeenCalledWith({ id: rule.id, http }); + }); + + it('deleteRule calls the deleteRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ deleteRule, rule }: ComponentOpts & { rule: Rule }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rule = mockRule(); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.deleteRules).toHaveBeenCalledTimes(1); + expect(ruleApi.deleteRules).toHaveBeenCalledWith({ ids: [rule.id], http }); + }); + + // bulk rules + it('muteRules calls the muteRules api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ muteRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rules = [mockRule(), mockRule()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.muteRules).toHaveBeenCalledTimes(1); + expect(ruleApi.muteRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http }); + }); + + it('unmuteRules calls the unmuteRules api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ unmuteRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rules = [mockRule({ muteAll: true }), mockRule({ muteAll: true })]; + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.unmuteRules).toHaveBeenCalledTimes(1); + expect(ruleApi.unmuteRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http }); + }); + + it('enableRules calls the muteRuless api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ enableRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rules = [ + mockRule({ enabled: false }), + mockRule({ enabled: true }), + mockRule({ enabled: false }), + ]; + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.enableRules).toHaveBeenCalledTimes(1); + expect(ruleApi.enableRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[2].id], http }); + }); + + it('disableRules calls the disableRules api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ disableRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rules = [mockRule(), mockRule()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.disableRules).toHaveBeenCalledTimes(1); + expect(ruleApi.disableRules).toHaveBeenCalledWith({ + ids: [rules[0].id, rules[1].id], + http, + }); + }); + + it('deleteRules calls the deleteRules api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ deleteRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const rules = [mockRule(), mockRule()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.deleteRules).toHaveBeenCalledTimes(1); + expect(ruleApi.deleteRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http }); + }); + + it('loadRule calls the loadRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ loadRule, ruleId }: ComponentOpts & { ruleId: Rule['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const ruleId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.loadRule).toHaveBeenCalledTimes(1); + expect(ruleApi.loadRule).toHaveBeenCalledWith({ ruleId, http }); + }); + + it('resolveRule calls the resolveRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ resolveRule, ruleId }: ComponentOpts & { ruleId: Rule['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const ruleId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.resolveRule).toHaveBeenCalledTimes(1); + expect(ruleApi.resolveRule).toHaveBeenCalledWith({ ruleId, http }); + }); + + it('loadRuleTypes calls the loadRuleTypes api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ loadRuleTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(ruleApi.loadRuleTypes).toHaveBeenCalledTimes(1); + expect(ruleApi.loadRuleTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockRule(overloads: Partial = {}): Rule { + return { + id: uuid.v4(), + enabled: true, + name: `rule-${uuid.v4()}`, + tags: [], + ruleTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + ...overloads, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx new file mode 100644 index 00000000000000..d9bafe5816d69a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.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 { + Rule, + RuleType, + RuleTaskState, + RuleSummary, + AlertingFrameworkHealth, + ResolvedRule, +} from '../../../../types'; +import { + deleteRules, + disableRules, + enableRules, + muteRules, + unmuteRules, + disableRule, + enableRule, + muteRule, + unmuteRule, + muteAlertInstance, + unmuteAlertInstance, + loadRule, + loadRuleState, + loadRuleSummary, + loadRuleTypes, + alertingFrameworkHealth, + resolveRule, +} from '../../../lib/rule_api'; +import { useKibana } from '../../../../common/lib/kibana'; + +export interface ComponentOpts { + muteRules: (rules: Rule[]) => Promise; + unmuteRules: (rules: Rule[]) => Promise; + enableRules: (rules: Rule[]) => Promise; + disableRules: (rules: Rule[]) => Promise; + deleteRules: (rules: Rule[]) => Promise<{ + successes: string[]; + errors: string[]; + }>; + muteRule: (rule: Rule) => Promise; + unmuteRule: (rule: Rule) => Promise; + muteAlertInstance: (rule: Rule, alertInstanceId: string) => Promise; + unmuteAlertInstance: (rule: Rule, alertInstanceId: string) => Promise; + enableRule: (rule: Rule) => Promise; + disableRule: (rule: Rule) => Promise; + deleteRule: (rule: Rule) => Promise<{ + successes: string[]; + errors: string[]; + }>; + loadRule: (id: Rule['id']) => Promise; + loadRuleState: (id: Rule['id']) => Promise; + loadRuleSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; + loadRuleTypes: () => Promise; + getHealth: () => Promise; + resolveRule: (id: Rule['id']) => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withBulkRuleOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useKibana().services; + return ( + + muteRules({ + http, + ids: items.filter((item) => !isRuleMuted(item)).map((item) => item.id), + }) + } + unmuteRules={async (items: Rule[]) => + unmuteRules({ http, ids: items.filter(isRuleMuted).map((item) => item.id) }) + } + enableRules={async (items: Rule[]) => + enableRules({ http, ids: items.filter(isRuleDisabled).map((item) => item.id) }) + } + disableRules={async (items: Rule[]) => + disableRules({ + http, + ids: items.filter((item) => !isRuleDisabled(item)).map((item) => item.id), + }) + } + deleteRules={async (items: Rule[]) => + deleteRules({ http, ids: items.map((item) => item.id) }) + } + muteRule={async (rule: Rule) => { + if (!isRuleMuted(rule)) { + return await muteRule({ http, id: rule.id }); + } + }} + unmuteRule={async (rule: Rule) => { + if (isRuleMuted(rule)) { + return await unmuteRule({ http, id: rule.id }); + } + }} + muteAlertInstance={async (rule: Rule, instanceId: string) => { + if (!isAlertInstanceMuted(rule, instanceId)) { + return muteAlertInstance({ http, id: rule.id, instanceId }); + } + }} + unmuteAlertInstance={async (rule: Rule, instanceId: string) => { + if (isAlertInstanceMuted(rule, instanceId)) { + return unmuteAlertInstance({ http, id: rule.id, instanceId }); + } + }} + enableRule={async (rule: Rule) => { + if (isRuleDisabled(rule)) { + return await enableRule({ http, id: rule.id }); + } + }} + disableRule={async (rule: Rule) => { + if (!isRuleDisabled(rule)) { + return await disableRule({ http, id: rule.id }); + } + }} + deleteRule={async (rule: Rule) => deleteRules({ http, ids: [rule.id] })} + loadRule={async (ruleId: Rule['id']) => loadRule({ http, ruleId })} + loadRuleState={async (ruleId: Rule['id']) => loadRuleState({ http, ruleId })} + loadRuleSummary={async (ruleId: Rule['id'], numberOfExecutions?: number) => + loadRuleSummary({ http, ruleId, numberOfExecutions }) + } + loadRuleTypes={async () => loadRuleTypes({ http })} + resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })} + getHealth={async () => alertingFrameworkHealth({ http })} + /> + ); + }; +} + +function isRuleDisabled(rule: Rule) { + return rule.enabled === false; +} + +function isRuleMuted(rule: Rule) { + return rule.muteAll === true; +} + +function isAlertInstanceMuted(rule: Rule, instanceId: string) { + return rule.mutedInstanceIds.findIndex((muted) => muted === instanceId) >= 0; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts index 4bf7f036ba10aa..6543d74ecd7a2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { ActionConnector, ActionTypeIndex, AlertAction } from '../../../types'; +import { ActionConnector, ActionTypeIndex, RuleAction } from '../../../types'; export const getValidConnectors = ( connectors: ActionConnector[], - actionItem: AlertAction, + actionItem: RuleAction, actionTypesIndex: ActionTypeIndex ): ActionConnector[] => { const actionType = actionTypesIndex[actionItem.actionTypeId]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index c3dd76094b5a9d..5b8a6ea569344e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -8,13 +8,16 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; -export type { ActionGroupWithCondition, AlertConditionsProps } from './alert_form/alert_conditions'; +export type { + ActionGroupWithCondition, + RuleConditionsProps as AlertConditionsProps, +} from './rule_form/rule_conditions'; -export const AlertConditions = lazy(() => import('./alert_form/alert_conditions')); -export const AlertConditionsGroup = lazy(() => import('./alert_form/alert_conditions_group')); +export const AlertConditions = lazy(() => import('./rule_form/rule_conditions')); +export const AlertConditionsGroup = lazy(() => import('./rule_form/rule_conditions_group')); -export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add'))); -export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit'))); +export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_add'))); +export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_edit'))); export const ConnectorAddFlyout = suspendedComponentWithProps( lazy(() => import('./action_connector_form/connector_add_flyout')) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_muted_switch.tsx similarity index 90% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_muted_switch.tsx index ce550243bcc37b..3f236ea25a19bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_muted_switch.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; -import { AlertListItem } from './alerts'; +import { AlertListItem } from './rule'; interface ComponentOpts { alert: AlertListItem; @@ -16,7 +16,7 @@ interface ComponentOpts { disabled: boolean; } -export const RuleMutedSwitch: React.FunctionComponent = ({ +export const AlertMutedSwitch: React.FunctionComponent = ({ alert, onMuteAction, disabled, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.scss similarity index 90% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.scss index 243ba5bc9c52c2..c4d2bf1bbb2d6c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.scss @@ -1,6 +1,6 @@ // Add truncation to heath status -.alertsList__health { +.rulesList__health { width: 100%; .euiFlexItem:last-of-type { @@ -13,4 +13,4 @@ .ruleDurationWarningIcon { bottom: $euiSizeXS; position: relative; -} \ No newline at end of file +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx similarity index 84% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index 909a562224ef76..ac103f113a8f1f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -10,8 +10,8 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Alerts, AlertListItem, alertToListItem } from './alerts'; -import { Rule, AlertSummary, AlertStatus, RuleType } from '../../../../types'; +import { RuleComponent, AlertListItem, alertToListItem } from './rule'; +import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types'; import { EuiBasicTable } from '@elastic/eui'; import { ExecutionDurationChart } from '../../common/components/execution_duration_chart'; @@ -33,18 +33,18 @@ beforeAll(() => { global.Date.now = jest.fn(() => fakeNow.getTime()); }); -describe('alerts', () => { - it('render a list of alerts', () => { +describe('rules', () => { + it('render a list of rules', () => { const rule = mockRule(); const ruleType = mockRuleType(); - const alertSummary = mockAlertSummary({ + const ruleSummary = mockRuleSummary({ alerts: { - first_alert: { + first_rule: { status: 'OK', muted: false, actionGroupId: 'default', }, - second_alert: { + second_rule: { status: 'Active', muted: false, actionGroupId: 'action group id unknown', @@ -52,47 +52,42 @@ describe('alerts', () => { }, }); - const alerts: AlertListItem[] = [ + const rules: AlertListItem[] = [ // active first - alertToListItem( - fakeNow.getTime(), - ruleType, - 'second_alert', - alertSummary.alerts.second_alert - ), + alertToListItem(fakeNow.getTime(), ruleType, 'second_rule', ruleSummary.alerts.second_rule), // ok second - alertToListItem(fakeNow.getTime(), ruleType, 'first_alert', alertSummary.alerts.first_alert), + alertToListItem(fakeNow.getTime(), ruleType, 'first_rule', ruleSummary.alerts.first_rule), ]; expect( shallow( - ) .find(EuiBasicTable) .prop('items') - ).toEqual(alerts); + ).toEqual(rules); }); it('render a hidden field with duration epoch', () => { const rule = mockRule(); const ruleType = mockRuleType(); - const alertSummary = mockAlertSummary(); + const ruleSummary = mockRuleSummary(); expect( shallow( - ) .find('[name="alertsDurationEpoch"]') @@ -100,7 +95,7 @@ describe('alerts', () => { ).toEqual(fake2MinutesAgo.getTime()); }); - it('render all active alerts', () => { + it('render all active rules', () => { const rule = mockRule(); const ruleType = mockRuleType(); const alerts: Record = { @@ -115,12 +110,12 @@ describe('alerts', () => { }; expect( shallow( - @@ -133,22 +128,22 @@ describe('alerts', () => { ]); }); - it('render all inactive alerts', () => { + it('render all inactive rules', () => { const rule = mockRule({ mutedInstanceIds: ['us-west', 'us-east'], }); const ruleType = mockRuleType(); - const alertUsWest: AlertStatus = { status: 'OK', muted: false }; - const alertUsEast: AlertStatus = { status: 'OK', muted: false }; + const ruleUsWest: AlertStatus = { status: 'OK', muted: false }; + const ruleUsEast: AlertStatus = { status: 'OK', muted: false }; expect( shallow( - { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertToListItem(fakeNow.getTime(), ruleType, 'us-west', alertUsWest), - alertToListItem(fakeNow.getTime(), ruleType, 'us-east', alertUsEast), + alertToListItem(fakeNow.getTime(), ruleType, 'us-west', ruleUsWest), + alertToListItem(fakeNow.getTime(), ruleType, 'us-east', ruleUsEast), ]); }); }); describe('alertToListItem', () => { - it('handles active alerts', () => { + it('handles active rules', () => { const ruleType = mockRuleType({ actionGroups: [ { id: 'default', name: 'Default Action Group' }, @@ -197,7 +192,7 @@ describe('alertToListItem', () => { }); }); - it('handles active alerts with no action group id', () => { + it('handles active rules with no action group id', () => { const ruleType = mockRuleType(); const start = fake2MinutesAgo; const alert: AlertStatus = { @@ -216,7 +211,7 @@ describe('alertToListItem', () => { }); }); - it('handles active muted alerts', () => { + it('handles active muted rules', () => { const ruleType = mockRuleType(); const start = fake2MinutesAgo; const alert: AlertStatus = { @@ -236,7 +231,7 @@ describe('alertToListItem', () => { }); }); - it('handles active alerts with start date', () => { + it('handles active rules with start date', () => { const ruleType = mockRuleType(); const alert: AlertStatus = { status: 'Active', @@ -254,7 +249,7 @@ describe('alertToListItem', () => { }); }); - it('handles muted inactive alerts', () => { + it('handles muted inactive rules', () => { const ruleType = mockRuleType(); const alert: AlertStatus = { status: 'OK', @@ -278,15 +273,15 @@ describe('execution duration overview', () => { executionStatus: { status: 'ok', lastExecutionDate: new Date('2020-08-20T19:23:38Z') }, }); const ruleType = mockRuleType(); - const alertSummary = mockAlertSummary(); + const ruleSummary = mockRuleSummary(); const wrapper = mountWithIntl( - ); @@ -304,17 +299,17 @@ describe('execution duration overview', () => { it('renders average execution duration', async () => { const rule = mockRule(); const ruleType = mockRuleType({ ruleTaskTimeout: '10m' }); - const alertSummary = mockAlertSummary({ + const ruleSummary = mockRuleSummary({ executionDuration: { average: 60284, valuesWithTimestamp: {} }, }); const wrapper = mountWithIntl( - ); @@ -335,17 +330,17 @@ describe('execution duration overview', () => { it('renders warning when average execution duration exceeds rule timeout', async () => { const rule = mockRule(); const ruleType = mockRuleType({ ruleTaskTimeout: '10m' }); - const alertSummary = mockAlertSummary({ + const ruleSummary = mockRuleSummary({ executionDuration: { average: 60284345, valuesWithTimestamp: {} }, }); const wrapper = mountWithIntl( - ); @@ -366,15 +361,15 @@ describe('execution duration overview', () => { it('renders execution duration chart', () => { const rule = mockRule(); const ruleType = mockRuleType(); - const alertSummary = mockAlertSummary(); + const ruleSummary = mockRuleSummary(); expect( shallow( - ) @@ -390,7 +385,7 @@ function mockRule(overloads: Partial = {}): Rule { enabled: true, name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], @@ -425,15 +420,15 @@ function mockRuleType(overloads: Partial = {}): RuleType { defaultActionGroupId: 'default', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, authorizedConsumers: {}, - producer: 'alerts', + producer: 'rules', minimumLicenseRequired: 'basic', enabledInLicense: true, ...overloads, }; } -function mockAlertSummary(overloads: Partial = {}): AlertSummary { - const summary: AlertSummary = { +function mockRuleSummary(overloads: Partial = {}): RuleSummary { + const summary: RuleSummary = { id: 'rule-id', name: 'rule-name', tags: ['tag-1', 'tag-2'], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx similarity index 82% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 9d4ef14dec64af..1a08f12c117435 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -28,36 +28,36 @@ import { AlertExecutionStatusErrorReasons, AlertStatusValues, } from '../../../../../../alerting/common'; -import { Rule, AlertSummary, AlertStatus, RuleType, Pagination } from '../../../../types'; +import { Rule, RuleSummary, AlertStatus, RuleType, Pagination } from '../../../../types'; import { - ComponentOpts as AlertApis, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; + ComponentOpts as RuleApis, + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; -import './alerts.scss'; -import { RuleMutedSwitch } from './rule_muted_switch'; -import { getHealthColor } from '../../alerts_list/components/alert_status_filter'; +import './rule.scss'; +import { AlertMutedSwitch } from './alert_muted_switch'; +import { getHealthColor } from '../../rules_list/components/rule_status_filter'; import { - alertsStatusesTranslationsMapping, + rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR, -} from '../../alerts_list/translations'; +} from '../../rules_list/translations'; import { formatMillisForDisplay, shouldShowDurationWarning, } from '../../../lib/execution_duration_utils'; import { ExecutionDurationChart } from '../../common/components/execution_duration_chart'; -type AlertsProps = { +type RuleProps = { rule: Rule; ruleType: RuleType; readOnly: boolean; - alertSummary: AlertSummary; + ruleSummary: RuleSummary; requestRefresh: () => Promise; numberOfExecutions: number; onChangeDuration: (length: number) => void; durationEpoch?: number; isLoadingChart?: boolean; -} & Pick; +} & Pick; export const alertsTableColumns = ( onMuteAction: (alert: AlertListItem) => Promise, @@ -65,7 +65,7 @@ export const alertsTableColumns = ( ) => [ { field: 'alert', - name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert', { + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.Alert', { defaultMessage: 'Alert', }), sortable: false, @@ -82,10 +82,9 @@ export const alertsTableColumns = ( }, { field: 'status', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status', - { defaultMessage: 'Status' } - ), + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.status', { + defaultMessage: 'Status', + }), width: '15%', render: (value: AlertListItemStatus) => { return ( @@ -104,7 +103,7 @@ export const alertsTableColumns = ( render: (value: Date | undefined) => { return value ? moment(value).format('D MMM YYYY @ HH:mm:ss') : ''; }, - name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start', { + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.start', { defaultMessage: 'Start', }), sortable: false, @@ -116,7 +115,7 @@ export const alertsTableColumns = ( return value ? durationAsString(moment.duration(value)) : ''; }, name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration', + 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.duration', { defaultMessage: 'Duration' } ), sortable: false, @@ -127,12 +126,12 @@ export const alertsTableColumns = ( field: '', align: RIGHT_ALIGNMENT, width: '60px', - name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute', { + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.mute', { defaultMessage: 'Mute', }), render: (alert: AlertListItem) => { return ( - await onMuteAction(alert)} alert={alert} @@ -150,11 +149,11 @@ function durationAsString(duration: Duration): string { .join(':'); } -export function Alerts({ +export function RuleComponent({ rule, ruleType, readOnly, - alertSummary, + ruleSummary, muteAlertInstance, unmuteAlertInstance, requestRefresh, @@ -162,13 +161,13 @@ export function Alerts({ onChangeDuration, durationEpoch = Date.now(), isLoadingChart, -}: AlertsProps) { +}: RuleProps) { const [pagination, setPagination] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE, }); - const alerts = Object.entries(alertSummary.alerts) + const alerts = Object.entries(ruleSummary.alerts) .map(([alertId, alert]) => alertToListItem(durationEpoch, ruleType, alertId, alert)) .sort((leftAlert, rightAlert) => leftAlert.sortPriority - rightAlert.sortPriority); @@ -183,7 +182,7 @@ export function Alerts({ const showDurationWarning = shouldShowDurationWarning( ruleType, - alertSummary.executionDuration.average + ruleSummary.executionDuration.average ); const healthColor = getHealthColor(rule.executionStatus.status); @@ -191,7 +190,7 @@ export function Alerts({ rule.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; const statusMessage = isLicenseError ? ALERT_STATUS_LICENSE_ERROR - : alertsStatusesTranslationsMapping[rule.executionStatus.status]; + : rulesStatusesTranslationsMapping[rule.executionStatus.status]; return ( <> @@ -212,7 +211,7 @@ export function Alerts({ } description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleLastExecutionDescription', + 'xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription', { defaultMessage: `Last response`, } @@ -239,7 +238,7 @@ export function Alerts({ type="alert" color="warning" content={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleTypeExcessDurationMessage', + 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage', { defaultMessage: `Duration exceeds the rule's expected run time.`, } @@ -249,12 +248,12 @@ export function Alerts({ )} - {formatMillisForDisplay(alertSummary.executionDuration.average)} + {formatMillisForDisplay(ruleSummary.executionDuration.average)} } description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.avgDurationDescription', + 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.avgDurationDescription', { defaultMessage: `Average duration`, } @@ -264,7 +263,7 @@ export function Alerts({ ); } -export const AlertsWithApi = withBulkAlertOperations(Alerts); +export const RuleWithApi = withBulkRuleOperations(RuleComponent); function getPage(items: any[], pagination: Pagination) { return chunk(items, pagination.size)[pagination.index] || []; @@ -323,12 +322,12 @@ export interface AlertListItem { } const ACTIVE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active', + 'xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active', { defaultMessage: 'Active' } ); const INACTIVE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive', + 'xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive', { defaultMessage: 'Recovered' } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index e12322c3b84ad4..4af24e9a44602d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from '@testing-library/react'; -import { AlertDetails } from './alert_details'; +import { RuleDetails } from './rule_details'; import { Rule, ActionType, RuleTypeModel, RuleType } from '../../../../types'; import { EuiBadge, @@ -35,7 +35,7 @@ jest.mock('react-router-dom', () => ({ push: jest.fn(), }), useLocation: () => ({ - pathname: '/triggersActions/alerts/', + pathname: '/triggersActions/rules/', }), })); @@ -45,17 +45,17 @@ jest.mock('../../../lib/action_connector_api', () => ({ jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), - hasSaveAlertsCapability: jest.fn(() => true), + hasSaveRulesCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); const useKibanaMock = useKibana as jest.Mocked; const ruleTypeRegistry = ruleTypeRegistryMock.create(); -const mockAlertApis = { - muteAlert: jest.fn(), - unmuteAlert: jest.fn(), - enableAlert: jest.fn(), - disableAlert: jest.fn(), +const mockRuleApis = { + muteRule: jest.fn(), + unmuteRule: jest.fn(), + enableRule: jest.fn(), + disableRule: jest.fn(), requestRefresh: jest.fn(), refreshToken: Date.now(), }; @@ -65,7 +65,7 @@ const authorizedConsumers = { }; const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' }; -const alertType: RuleType = { +const ruleType: RuleType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -78,27 +78,27 @@ const alertType: RuleType = { enabledInLicense: true, }; -describe('alert_details', () => { - it('renders the alert name as a title', () => { - const alert = mockAlert(); +describe('rule_details', () => { + it('renders the rule name as a title', () => { + const rule = mockRule(); expect( shallow( - + ).find('EuiPageHeader') ).toBeTruthy(); }); - it('renders the alert type badge', () => { - const alert = mockAlert(); + it('renders the rule type badge', () => { + const rule = mockRule(); expect( shallow( - - ).find({alertType.name}) + + ).find({ruleType.name}) ).toBeTruthy(); }); - it('renders the alert error banner with error message, when alert status is an error', () => { - const alert = mockAlert({ + it('renders the rule error banner with error message, when rule status is an error', () => { + const rule = mockRule({ executionStatus: { status: 'error', lastExecutionDate: new Date('2020-08-20T19:23:38Z'), @@ -110,9 +110,9 @@ describe('alert_details', () => { }); expect( shallow( - + ).containsMatchingElement( - + {'test'} ) @@ -120,8 +120,8 @@ describe('alert_details', () => { }); describe('actions', () => { - it('renders an alert action', () => { - const alert = mockAlert({ + it('renders an rule action', () => { + const rule = mockRule({ actions: [ { group: 'default', @@ -145,11 +145,11 @@ describe('alert_details', () => { expect( shallow( - ).containsMatchingElement( @@ -159,8 +159,8 @@ describe('alert_details', () => { ).toBeTruthy(); }); - it('renders a counter for multiple alert action', () => { - const alert = mockAlert({ + it('renders a counter for multiple rule action', () => { + const rule = mockRule({ actions: [ { group: 'default', @@ -196,12 +196,7 @@ describe('alert_details', () => { ]; const details = shallow( - + ); expect( @@ -223,19 +218,19 @@ describe('alert_details', () => { }); describe('links', () => { - it('links to the app that created the alert', () => { - const alert = mockAlert(); + it('links to the app that created the rule', () => { + const rule = mockRule(); expect( shallow( - + ).find('ViewInApp') ).toBeTruthy(); }); it('links to the Edit flyout', () => { - const alert = mockAlert(); + const rule = mockRule(); const pageHeaderProps = shallow( - + ) .find('EuiPageHeader') .props() as EuiPageHeaderProps; @@ -243,7 +238,7 @@ describe('alert_details', () => { expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { > @@ -262,12 +257,12 @@ describe('alert_details', () => { }); describe('disable button', () => { - it('should render a disable button when alert is enabled', () => { - const alert = mockAlert({ + it('should render a disable button when rule is enabled', () => { + const rule = mockRule({ enabled: true, }); const enableButton = shallow( - + ) .find(EuiSwitch) .find('[name="enable"]') @@ -279,12 +274,12 @@ describe('disable button', () => { }); }); - it('should render a enable button and empty state when alert is disabled', async () => { - const alert = mockAlert({ + it('should render a enable button and empty state when rule is disabled', async () => { + const rule = mockRule({ enabled: false, }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -309,21 +304,21 @@ describe('disable button', () => { wrapper.update(); }); - expect(mockAlertApis.enableAlert).toHaveBeenCalledTimes(1); + expect(mockRuleApis.enableRule).toHaveBeenCalledTimes(1); }); - it('should disable the alert when alert is enabled and button is clicked', () => { - const alert = mockAlert({ + it('should disable the rule when rule is enabled and button is clicked', () => { + const rule = mockRule({ enabled: true, }); - const disableAlert = jest.fn(); + const disableRule = jest.fn(); const enableButton = shallow( - ) .find(EuiSwitch) @@ -333,23 +328,23 @@ describe('disable button', () => { enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); - expect(disableAlert).toHaveBeenCalledTimes(0); + expect(disableRule).toHaveBeenCalledTimes(0); handler!({} as React.FormEvent); - expect(disableAlert).toHaveBeenCalledTimes(1); + expect(disableRule).toHaveBeenCalledTimes(1); }); - it('should enable the alert when alert is disabled and button is clicked', () => { - const alert = mockAlert({ + it('should enable the rule when rule is disabled and button is clicked', () => { + const rule = mockRule({ enabled: false, }); - const enableAlert = jest.fn(); + const enableRule = jest.fn(); const enableButton = shallow( - ) .find(EuiSwitch) @@ -359,13 +354,13 @@ describe('disable button', () => { enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); - expect(enableAlert).toHaveBeenCalledTimes(0); + expect(enableRule).toHaveBeenCalledTimes(0); handler!({} as React.FormEvent); - expect(enableAlert).toHaveBeenCalledTimes(1); + expect(enableRule).toHaveBeenCalledTimes(1); }); - it('should reset error banner dismissal after re-enabling the alert', async () => { - const alert = mockAlert({ + it('should reset error banner dismissal after re-enabling the rule', async () => { + const rule = mockRule({ enabled: true, executionStatus: { status: 'error', @@ -377,16 +372,16 @@ describe('disable button', () => { }, }); - const disableAlert = jest.fn(); - const enableAlert = jest.fn(); + const disableRule = jest.fn(); + const enableRule = jest.fn(); const wrapper = mountWithIntl( - ); @@ -401,31 +396,31 @@ describe('disable button', () => { await nextTick(); }); - // Disable the alert + // Disable the rule await act(async () => { wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click'); await nextTick(); }); - expect(disableAlert).toHaveBeenCalled(); + expect(disableRule).toHaveBeenCalled(); await act(async () => { await nextTick(); wrapper.update(); }); - // Enable the alert + // Enable the rule await act(async () => { wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click'); await nextTick(); }); - expect(enableAlert).toHaveBeenCalled(); + expect(enableRule).toHaveBeenCalled(); // Ensure error banner is back expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0); }); it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => { - const alert = mockAlert({ + const rule = mockRule({ enabled: true, executionStatus: { status: 'error', @@ -437,18 +432,18 @@ describe('disable button', () => { }, }); - const disableAlert = jest.fn(async () => { + const disableRule = jest.fn(async () => { await new Promise((resolve) => setTimeout(resolve, 6000)); }); - const enableAlert = jest.fn(); + const enableRule = jest.fn(); const wrapper = mountWithIntl( - ); @@ -463,19 +458,19 @@ describe('disable button', () => { await nextTick(); }); - // Disable the alert + // Disable the rule await act(async () => { wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click'); await nextTick(); }); - expect(disableAlert).toHaveBeenCalled(); + expect(disableRule).toHaveBeenCalled(); await act(async () => { await nextTick(); wrapper.update(); }); - // Enable the alert + // Enable the rule await act(async () => { expect(wrapper.find('[data-test-subj="enableSpinner"]').length).toBeGreaterThan(0); await nextTick(); @@ -484,13 +479,13 @@ describe('disable button', () => { }); describe('mute button', () => { - it('should render an mute button when alert is enabled', () => { - const alert = mockAlert({ + it('should render an mute button when rule is enabled', () => { + const rule = mockRule({ enabled: true, muteAll: false, }); const enableButton = shallow( - + ) .find(EuiSwitch) .find('[name="mute"]') @@ -501,13 +496,13 @@ describe('mute button', () => { }); }); - it('should render an muted button when alert is muted', () => { - const alert = mockAlert({ + it('should render an muted button when rule is muted', () => { + const rule = mockRule({ enabled: true, muteAll: true, }); const enableButton = shallow( - + ) .find(EuiSwitch) .find('[name="mute"]') @@ -518,19 +513,19 @@ describe('mute button', () => { }); }); - it('should mute the alert when alert is unmuted and button is clicked', () => { - const alert = mockAlert({ + it('should mute the rule when rule is unmuted and button is clicked', () => { + const rule = mockRule({ enabled: true, muteAll: false, }); - const muteAlert = jest.fn(); + const muteRule = jest.fn(); const enableButton = shallow( - ) .find(EuiSwitch) @@ -539,24 +534,24 @@ describe('mute button', () => { enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); - expect(muteAlert).toHaveBeenCalledTimes(0); + expect(muteRule).toHaveBeenCalledTimes(0); handler!({} as React.FormEvent); - expect(muteAlert).toHaveBeenCalledTimes(1); + expect(muteRule).toHaveBeenCalledTimes(1); }); - it('should unmute the alert when alert is muted and button is clicked', () => { - const alert = mockAlert({ + it('should unmute the rule when rule is muted and button is clicked', () => { + const rule = mockRule({ enabled: true, muteAll: true, }); - const unmuteAlert = jest.fn(); + const unmuteRule = jest.fn(); const enableButton = shallow( - ) .find(EuiSwitch) @@ -565,18 +560,18 @@ describe('mute button', () => { enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); - expect(unmuteAlert).toHaveBeenCalledTimes(0); + expect(unmuteRule).toHaveBeenCalledTimes(0); handler!({} as React.FormEvent); - expect(unmuteAlert).toHaveBeenCalledTimes(1); + expect(unmuteRule).toHaveBeenCalledTimes(1); }); - it('should disabled mute button when alert is disabled', () => { - const alert = mockAlert({ + it('should disabled mute button when rule is disabled', () => { + const rule = mockRule({ enabled: false, muteAll: false, }); const enableButton = shallow( - + ) .find(EuiSwitch) .find('[name="mute"]') @@ -600,10 +595,10 @@ describe('edit button', () => { }, ]; ruleTypeRegistry.has.mockReturnValue(true); - const alertTypeR: RuleTypeModel = { - id: 'my-alert-type', + const ruleTypeR: RuleTypeModel = { + id: 'my-rule-type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: () => { return { errors: {} }; @@ -611,11 +606,11 @@ describe('edit button', () => { ruleParamsExpression: jest.fn(), requiresAppContext: false, }; - ruleTypeRegistry.get.mockReturnValue(alertTypeR); + ruleTypeRegistry.get.mockReturnValue(ruleTypeR); useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - it('should render an edit button when alert and actions are editable', () => { - const alert = mockAlert({ + it('should render an edit button when rule and actions are editable', () => { + const rule = mockRule({ enabled: true, muteAll: false, actions: [ @@ -628,12 +623,7 @@ describe('edit button', () => { ], }); const pageHeaderProps = shallow( - + ) .find('EuiPageHeader') .props() as EuiPageHeaderProps; @@ -641,7 +631,7 @@ describe('edit button', () => { expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { > @@ -657,10 +647,10 @@ describe('edit button', () => { `); }); - it('should not render an edit button when alert editable but actions arent', () => { + it('should not render an edit button when rule editable but actions arent', () => { const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); hasExecuteActionsCapability.mockReturnValueOnce(false); - const alert = mockAlert({ + const rule = mockRule({ enabled: true, muteAll: false, actions: [ @@ -674,12 +664,7 @@ describe('edit button', () => { }); expect( shallow( - + ) .find(EuiButtonEmpty) .find('[name="edit"]') @@ -688,21 +673,16 @@ describe('edit button', () => { ).toBeFalsy(); }); - it('should render an edit button when alert editable but actions arent when there are no actions on the alert', async () => { + it('should render an edit button when rule editable but actions arent when there are no actions on the rule', async () => { const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); hasExecuteActionsCapability.mockReturnValueOnce(false); - const alert = mockAlert({ + const rule = mockRule({ enabled: true, muteAll: false, actions: [], }); const pageHeaderProps = shallow( - + ) .find('EuiPageHeader') .props() as EuiPageHeaderProps; @@ -710,7 +690,7 @@ describe('edit button', () => { expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { > @@ -739,10 +719,10 @@ describe('broken connector indicator', () => { }, ]; ruleTypeRegistry.has.mockReturnValue(true); - const alertTypeR: RuleTypeModel = { - id: 'my-alert-type', + const ruleTypeR: RuleTypeModel = { + id: 'my-rule-type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: () => { return { errors: {} }; @@ -750,7 +730,7 @@ describe('broken connector indicator', () => { ruleParamsExpression: jest.fn(), requiresAppContext: false, }; - ruleTypeRegistry.get.mockReturnValue(alertTypeR); + ruleTypeRegistry.get.mockReturnValue(ruleTypeR); useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); loadAllActions.mockResolvedValue([ @@ -775,7 +755,7 @@ describe('broken connector indicator', () => { ]); it('should not render broken connector indicator or warning if all rule actions connectors exist', async () => { - const alert = mockAlert({ + const rule = mockRule({ enabled: true, muteAll: false, actions: [ @@ -794,12 +774,7 @@ describe('broken connector indicator', () => { ], }); const wrapper = mountWithIntl( - + ); await act(async () => { await nextTick(); @@ -816,7 +791,7 @@ describe('broken connector indicator', () => { }); it('should render broken connector indicator and warning if any rule actions connector does not exist', async () => { - const alert = mockAlert({ + const rule = mockRule({ enabled: true, muteAll: false, actions: [ @@ -841,12 +816,7 @@ describe('broken connector indicator', () => { ], }); const wrapper = mountWithIntl( - + ); await act(async () => { await nextTick(); @@ -867,7 +837,7 @@ describe('broken connector indicator', () => { }); it('should render broken connector indicator and warning with no edit button if any rule actions connector does not exist and user has no edit access', async () => { - const alert = mockAlert({ + const rule = mockRule({ enabled: true, muteAll: false, actions: [ @@ -894,12 +864,7 @@ describe('broken connector indicator', () => { const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); hasExecuteActionsCapability.mockReturnValue(false); const wrapper = mountWithIntl( - + ); await act(async () => { await nextTick(); @@ -922,14 +887,14 @@ describe('broken connector indicator', () => { describe('refresh button', () => { it('should call requestRefresh when clicked', async () => { - const alert = mockAlert(); + const rule = mockRule(); const requestRefresh = jest.fn(); const wrapper = mountWithIntl( - ); @@ -938,7 +903,7 @@ describe('refresh button', () => { await nextTick(); wrapper.update(); }); - const refreshButton = wrapper.find('[data-test-subj="refreshAlertsButton"]').first(); + const refreshButton = wrapper.find('[data-test-subj="refreshRulesButton"]').first(); expect(refreshButton.exists()).toBeTruthy(); refreshButton.simulate('click'); @@ -946,13 +911,13 @@ describe('refresh button', () => { }); }); -function mockAlert(overloads: Partial = {}): Rule { +function mockRule(overloads: Partial = {}): Rule { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: ALERTS_FEATURE_ID, schedule: { interval: '1m' }, actions: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx similarity index 70% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index 1bb979ee86052b..becf1376c89002 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -29,38 +29,38 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; -import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; +import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; import { Rule, RuleType, ActionType, ActionConnector } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; -import { AlertsRouteWithApi } from './alerts_route'; + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; +import { RuleRouteWithApi } from './rule_route'; import { ViewInApp } from './view_in_app'; -import { AlertEdit } from '../../alert_form'; +import { RuleEdit } from '../../rule_form'; import { routeToRuleDetails } from '../../../constants'; -import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations'; +import { rulesErrorReasonTranslationsMapping } from '../../rules_list/translations'; import { useKibana } from '../../../../common/lib/kibana'; -import { alertReducer } from '../../alert_form/alert_reducer'; +import { ruleReducer } from '../../rule_form/rule_reducer'; import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api'; -export type AlertDetailsProps = { - alert: Rule; - alertType: RuleType; +export type RuleDetailsProps = { + rule: Rule; + ruleType: RuleType; actionTypes: ActionType[]; requestRefresh: () => Promise; refreshToken?: number; -} & Pick; +} & Pick; -export const AlertDetails: React.FunctionComponent = ({ - alert, - alertType, +export const RuleDetails: React.FunctionComponent = ({ + rule, + ruleType, actionTypes, - disableAlert, - enableAlert, - unmuteAlert, - muteAlert, + disableRule, + enableRule, + unmuteRule, + muteRule, requestRefresh, refreshToken, }) => { @@ -73,9 +73,9 @@ export const AlertDetails: React.FunctionComponent = ({ chrome, http, } = useKibana().services; - const [{}, dispatch] = useReducer(alertReducer, { alert }); - const setInitialAlert = (value: Rule) => { - dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); + const [{}, dispatch] = useReducer(ruleReducer, { rule }); + const setInitialRule = (value: Rule) => { + dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); }; const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = @@ -84,10 +84,10 @@ export const AlertDetails: React.FunctionComponent = ({ // Set breadcrumb and page title useEffect(() => { setBreadcrumbs([ - getAlertingSectionBreadcrumb('alerts'), - getAlertDetailsBreadcrumb(alert.id, alert.name), + getAlertingSectionBreadcrumb('rules'), + getRuleDetailsBreadcrumb(rule.id, rule.name), ]); - chrome.docTitle.change(getCurrentDocTitle('alerts')); + chrome.docTitle.change(getCurrentDocTitle('rules')); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -102,7 +102,7 @@ export const AlertDetails: React.FunctionComponent = ({ } if (loadedConnectors.length > 0) { - const hasActionWithBrokenConnector = alert.actions.some( + const hasActionWithBrokenConnector = rule.actions.some( (action) => !loadedConnectors.find((connector) => connector.id === action.id) ); if (setHasActionsWithBrokenConnector) { @@ -114,38 +114,38 @@ export const AlertDetails: React.FunctionComponent = ({ }, []); const canExecuteActions = hasExecuteActionsCapability(capabilities); - const canSaveAlert = - hasAllPrivilege(alert, alertType) && - // if the alert has actions, can the user save the alert's action params - (canExecuteActions || (!canExecuteActions && alert.actions.length === 0)); + const canSaveRule = + hasAllPrivilege(rule, ruleType) && + // if the rule has actions, can the user save the rule's action params + (canExecuteActions || (!canExecuteActions && rule.actions.length === 0)); const actionTypesByTypeId = keyBy(actionTypes, 'id'); const hasEditButton = - // can the user save the alert - canSaveAlert && - // is this alert type editable from within Alerts Management - (ruleTypeRegistry.has(alert.alertTypeId) - ? !ruleTypeRegistry.get(alert.alertTypeId).requiresAppContext + // can the user save the rule + canSaveRule && + // is this rule type editable from within Rules Management + (ruleTypeRegistry.has(rule.ruleTypeId) + ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext : false); - const alertActions = alert.actions; - const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId))); - const [isEnabled, setIsEnabled] = useState(alert.enabled); + const ruleActions = rule.actions; + const uniqueActions = Array.from(new Set(ruleActions.map((item: any) => item.actionTypeId))); + const [isEnabled, setIsEnabled] = useState(rule.enabled); const [isEnabledUpdating, setIsEnabledUpdating] = useState(false); const [isMutedUpdating, setIsMutedUpdating] = useState(false); - const [isMuted, setIsMuted] = useState(alert.muteAll); + const [isMuted, setIsMuted] = useState(rule.muteAll); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); - const [dissmissAlertErrors, setDissmissAlertErrors] = useState(false); + const [dissmissRuleErrors, setDissmissRuleErrors] = useState(false); - const setAlert = async () => { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + const setRule = async () => { + history.push(routeToRuleDetails.replace(`:ruleId`, rule.id)); }; - const getAlertStatusErrorReasonText = () => { - if (alert.executionStatus.error && alert.executionStatus.error.reason) { - return alertsErrorReasonTranslationsMapping[alert.executionStatus.error.reason]; + const getRuleStatusErrorReasonText = () => { + if (rule.executionStatus.error && rule.executionStatus.error.reason) { + return rulesErrorReasonTranslationsMapping[rule.executionStatus.error.reason]; } else { - return alertsErrorReasonTranslationsMapping.unknown; + return rulesErrorReasonTranslationsMapping.unknown; } }; @@ -153,28 +153,28 @@ export const AlertDetails: React.FunctionComponent = ({ ? [ <> setEditFlyoutVisibility(true)} name="edit" - disabled={!alertType.enabledInLicense} + disabled={!ruleType.enabledInLicense} > {editFlyoutVisible && ( - { - setInitialAlert(alert); + setInitialRule(rule); setEditFlyoutVisibility(false); }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} - ruleType={alertType} - onSave={setAlert} + ruleType={ruleType} + onSave={setRule} /> )} , @@ -184,26 +184,26 @@ export const AlertDetails: React.FunctionComponent = ({ return ( <> } rightSideItems={[ - , + , , @@ -217,29 +217,29 @@ export const AlertDetails: React.FunctionComponent = ({

- {alertType.name} + {ruleType.name}
{uniqueActions && uniqueActions.length ? ( <> {' '} {hasActionsWithBrokenConnector && ( = ({ @@ -285,26 +285,26 @@ export const AlertDetails: React.FunctionComponent = ({ ) : ( { setIsEnabledUpdating(true); if (isEnabled) { setIsEnabled(false); - await disableAlert(alert); + await disableRule(rule); // Reset dismiss if previously clicked - setDissmissAlertErrors(false); + setDissmissRuleErrors(false); } else { setIsEnabled(true); - await enableAlert(alert); + await enableRule(rule); } requestRefresh(); setIsEnabledUpdating(false); }} label={ } @@ -321,7 +321,7 @@ export const AlertDetails: React.FunctionComponent = ({ @@ -331,23 +331,23 @@ export const AlertDetails: React.FunctionComponent = ({ { setIsMutedUpdating(true); if (isMuted) { setIsMuted(false); - await unmuteAlert(alert); + await unmuteRule(rule); } else { setIsMuted(true); - await muteAlert(alert); + await muteRule(rule); } requestRefresh(); setIsMutedUpdating(false); }} label={ } @@ -357,18 +357,18 @@ export const AlertDetails: React.FunctionComponent = ({ - {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + {rule.enabled && !dissmissRuleErrors && rule.executionStatus.status === 'error' ? ( - - {alert.executionStatus.error?.message} + + {rule.executionStatus.error?.message} @@ -376,15 +376,15 @@ export const AlertDetails: React.FunctionComponent = ({ setDissmissAlertErrors(true)} + onClick={() => setDissmissRuleErrors(true)} > - {alert.executionStatus.error?.reason === + {rule.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License && ( = ({ target="_blank" > @@ -413,7 +413,7 @@ export const AlertDetails: React.FunctionComponent = ({ data-test-subj="actionWithBrokenConnectorWarningBanner" size="s" title={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerTitle', + 'xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle', { defaultMessage: 'There is an issue with one of the connectors associated with this rule.', @@ -429,7 +429,7 @@ export const AlertDetails: React.FunctionComponent = ({ onClick={() => setEditFlyoutVisibility(true)} > @@ -442,13 +442,13 @@ export const AlertDetails: React.FunctionComponent = ({ )} - {alert.enabled ? ( - ) : ( <> @@ -459,7 +459,7 @@ export const AlertDetails: React.FunctionComponent = ({ title={

@@ -468,7 +468,7 @@ export const AlertDetails: React.FunctionComponent = ({ <>

@@ -483,7 +483,7 @@ export const AlertDetails: React.FunctionComponent = ({ onClick={async () => { setIsEnabledUpdating(true); setIsEnabled(true); - await enableAlert(alert); + await enableRule(rule); requestRefresh(); setIsEnabledUpdating(false); }} @@ -502,4 +502,4 @@ export const AlertDetails: React.FunctionComponent = ({ ); }; -export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails); +export const RuleDetailsWithApi = withBulkRuleOperations(RuleDetails); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.test.tsx similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.test.tsx index 9bfb8f20744de4..796b2e107a6fef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.test.tsx @@ -12,14 +12,14 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory, createLocation } from 'history'; import { ToastsApi } from 'kibana/public'; -import { AlertDetailsRoute, getRuleData } from './alert_details_route'; +import { RuleDetailsRoute, getRuleData } from './rule_details_route'; import { Rule } from '../../../../types'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { spacesPluginMock } from '../../../../../../spaces/public/mocks'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); -describe('alert_details_route', () => { +describe('rule_details_route', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -36,7 +36,7 @@ describe('alert_details_route', () => { expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); @@ -53,13 +53,12 @@ describe('alert_details_route', () => { alias_target_id: rule.id, })); const wrapper = mountWithIntl( - + ); await act(async () => { await nextTick(); wrapper.update(); }); - expect(resolveRule).toHaveBeenCalledWith(rule.id); expect((spacesMock as any).ui.redirectLegacyUrl).toHaveBeenCalledWith( `insightsAndAlerting/triggersActions/rule/new_id`, @@ -71,13 +70,13 @@ describe('alert_details_route', () => { await setup(); const rule = mockRule(); const ruleType = { - id: rule.alertTypeId, + id: rule.ruleTypeId, name: 'type name', authorizedConsumers: ['consumer'], }; - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); - loadAlertTypes.mockImplementationOnce(async () => [ruleType]); + loadRuleTypes.mockImplementationOnce(async () => [ruleType]); loadActionTypes.mockImplementation(async () => []); resolveRule.mockImplementationOnce(async () => ({ ...rule, @@ -86,9 +85,9 @@ describe('alert_details_route', () => { alias_target_id: rule.id, })); const wrapper = mountWithIntl( - ); await act(async () => { @@ -113,8 +112,8 @@ describe('getRuleData useEffect handler', () => { it('fetches rule', async () => { const rule = mockRule(); - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementationOnce(async () => rule); @@ -124,17 +123,17 @@ describe('getRuleData useEffect handler', () => { await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); expect(resolveRule).toHaveBeenCalledWith(rule.id); - expect(setAlert).toHaveBeenCalledWith(rule); + expect(setRule).toHaveBeenCalledWith(rule); }); it('fetches rule and connector types', async () => { @@ -154,14 +153,14 @@ describe('getRuleData useEffect handler', () => { ], }); const ruleType = { - id: rule.alertTypeId, + id: rule.ruleTypeId, name: 'type name', }; - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => [ruleType]); + loadRuleTypes.mockImplementation(async () => [ruleType]); loadActionTypes.mockImplementation(async () => [connectorType]); const toastNotifications = { @@ -170,21 +169,21 @@ describe('getRuleData useEffect handler', () => { await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); - expect(loadAlertTypes).toHaveBeenCalledTimes(1); + expect(loadRuleTypes).toHaveBeenCalledTimes(1); expect(loadActionTypes).toHaveBeenCalledTimes(1); expect(resolveRule).toHaveBeenCalled(); - expect(setAlert).toHaveBeenCalledWith(rule); - expect(setAlertType).toHaveBeenCalledWith(ruleType); + expect(setRule).toHaveBeenCalledWith(rule); + expect(setRuleType).toHaveBeenCalledWith(ruleType); expect(setActionTypes).toHaveBeenCalledWith([connectorType]); }); @@ -205,8 +204,8 @@ describe('getRuleData useEffect handler', () => { ], }); - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => { throw new Error('OMG'); @@ -217,11 +216,11 @@ describe('getRuleData useEffect handler', () => { } as unknown as ToastsApi; await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); @@ -248,12 +247,12 @@ describe('getRuleData useEffect handler', () => { ], }); - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => { + loadRuleTypes.mockImplementation(async () => { throw new Error('OMG no rule type'); }); loadActionTypes.mockImplementation(async () => [connectorType]); @@ -263,11 +262,11 @@ describe('getRuleData useEffect handler', () => { } as unknown as ToastsApi; await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); @@ -294,16 +293,16 @@ describe('getRuleData useEffect handler', () => { ], }); const ruleType = { - id: rule.alertTypeId, + id: rule.ruleTypeId, name: 'type name', }; - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => [ruleType]); + loadRuleTypes.mockImplementation(async () => [ruleType]); loadActionTypes.mockImplementation(async () => { throw new Error('OMG no connector type'); }); @@ -313,11 +312,11 @@ describe('getRuleData useEffect handler', () => { } as unknown as ToastsApi; await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); @@ -349,11 +348,11 @@ describe('getRuleData useEffect handler', () => { name: 'type name', }; - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => [ruleType]); + loadRuleTypes.mockImplementation(async () => [ruleType]); loadActionTypes.mockImplementation(async () => [connectorType]); const toastNotifications = { @@ -361,17 +360,17 @@ describe('getRuleData useEffect handler', () => { } as unknown as ToastsApi; await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: `Unable to load rule: Invalid Rule Type: ${rule.alertTypeId}`, + title: `Unable to load rule: Invalid Rule Type: ${rule.ruleTypeId}`, }); }); @@ -408,11 +407,11 @@ describe('getRuleData useEffect handler', () => { name: 'type name', }; - const { loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); - const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + const { loadRuleTypes, loadActionTypes, resolveRule } = mockApis(); + const { setRule, setRuleType, setActionTypes } = mockStateSetter(); resolveRule.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => [ruleType]); + loadRuleTypes.mockImplementation(async () => [ruleType]); loadActionTypes.mockImplementation(async () => [availableConnectorType]); const toastNotifications = { @@ -420,11 +419,11 @@ describe('getRuleData useEffect handler', () => { } as unknown as ToastsApi; await getRuleData( rule.id, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toastNotifications ); @@ -437,8 +436,8 @@ describe('getRuleData useEffect handler', () => { function mockApis() { return { - loadAlert: jest.fn(), - loadAlertTypes: jest.fn(), + loadRule: jest.fn(), + loadRuleTypes: jest.fn(), loadActionTypes: jest.fn(), resolveRule: jest.fn(), }; @@ -446,8 +445,8 @@ function mockApis() { function mockStateSetter() { return { - setAlert: jest.fn(), - setAlertType: jest.fn(), + setRule: jest.fn(), + setRuleType: jest.fn(), setActionTypes: jest.fn(), }; } @@ -470,7 +469,7 @@ function mockRule(overloads: Partial = {}): Rule { enabled: true, name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx similarity index 66% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx index 2177275a0fb2f4..c5cdff71506f3a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details_route.tsx @@ -11,12 +11,12 @@ import { RouteComponentProps } from 'react-router-dom'; import { ToastsApi } from 'kibana/public'; import { EuiSpacer } from '@elastic/eui'; import { RuleType, ActionType, ResolvedRule } from '../../../../types'; -import { AlertDetailsWithApi as AlertDetails } from './alert_details'; +import { RuleDetailsWithApi as RuleDetails } from './rule_details'; import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; import { - ComponentOpts as AlertApis, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; + ComponentOpts as RuleApis, + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; import { ComponentOpts as ActionApis, withActionOperations, @@ -24,17 +24,17 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; -type AlertDetailsRouteProps = RouteComponentProps<{ +type RuleDetailsRouteProps = RouteComponentProps<{ ruleId: string; }> & Pick & - Pick; + Pick; -export const AlertDetailsRoute: React.FunctionComponent = ({ +export const RuleDetailsRoute: React.FunctionComponent = ({ match: { params: { ruleId }, }, - loadAlertTypes, + loadRuleTypes, loadActionTypes, resolveRule, }) => { @@ -46,44 +46,44 @@ export const AlertDetailsRoute: React.FunctionComponent const { basePath } = http; - const [alert, setAlert] = useState(null); - const [alertType, setAlertType] = useState(null); + const [rule, setRule] = useState(null); + const [ruleType, setRuleType] = useState(null); const [actionTypes, setActionTypes] = useState(null); const [refreshToken, requestRefresh] = React.useState(); useEffect(() => { getRuleData( ruleId, - loadAlertTypes, + loadRuleTypes, resolveRule, loadActionTypes, - setAlert, - setAlertType, + setRule, + setRuleType, setActionTypes, toasts ); - }, [ruleId, http, loadActionTypes, loadAlertTypes, resolveRule, toasts, refreshToken]); + }, [ruleId, http, loadActionTypes, loadRuleTypes, resolveRule, toasts, refreshToken]); useEffect(() => { - if (alert) { - const outcome = (alert as ResolvedRule).outcome; + if (rule) { + const outcome = (rule as ResolvedRule).outcome; if (spacesApi && outcome === 'aliasMatch') { // This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast. - const path = basePath.prepend(`insightsAndAlerting/triggersActions/rule/${alert.id}`); + const path = basePath.prepend(`insightsAndAlerting/triggersActions/rule/${rule.id}`); spacesApi.ui.redirectLegacyUrl( path, - i18n.translate('xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', { + i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun', { defaultMessage: 'rule', }) ); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alert]); + }, [rule]); const getLegacyUrlConflictCallout = () => { - const outcome = (alert as ResolvedRule).outcome; + const outcome = (rule as ResolvedRule).outcome; if (spacesApi && outcome === 'conflict') { - const aliasTargetId = (alert as ResolvedRule).alias_target_id!; // This is always defined if outcome === 'conflict' + const aliasTargetId = (rule as ResolvedRule).alias_target_id!; // This is always defined if outcome === 'conflict' // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a // callout with a warning for the user, and provide a way for them to navigate to the other rule. const otherRulePath = basePath.prepend( @@ -94,12 +94,12 @@ export const AlertDetailsRoute: React.FunctionComponent {spacesApi.ui.components.getLegacyUrlConflict({ objectNoun: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + 'xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun', { defaultMessage: 'rule', } ), - currentObjectId: alert?.id!, + currentObjectId: rule?.id!, otherObjectId: aliasTargetId, otherObjectPath: otherRulePath, })} @@ -109,12 +109,12 @@ export const AlertDetailsRoute: React.FunctionComponent return null; }; - return alert && alertType && actionTypes ? ( + return rule && ruleType && actionTypes ? ( <> {getLegacyUrlConflictCallout()} - requestRefresh(Date.now())} refreshToken={refreshToken} @@ -127,22 +127,22 @@ export const AlertDetailsRoute: React.FunctionComponent export async function getRuleData( ruleId: string, - loadAlertTypes: AlertApis['loadAlertTypes'], - resolveRule: AlertApis['resolveRule'], + loadRuleTypes: RuleApis['loadRuleTypes'], + resolveRule: RuleApis['resolveRule'], loadActionTypes: ActionApis['loadActionTypes'], - setAlert: React.Dispatch>, - setAlertType: React.Dispatch>, + setRule: React.Dispatch>, + setRuleType: React.Dispatch>, setActionTypes: React.Dispatch>, toasts: Pick ) { try { const loadedRule: ResolvedRule = await resolveRule(ruleId); - setAlert(loadedRule); + setRule(loadedRule); - const [loadedAlertType, loadedActionTypes] = await Promise.all([ - loadAlertTypes() - .then((types) => types.find((type) => type.id === loadedRule.alertTypeId)) - .then(throwIfAbsent(`Invalid Rule Type: ${loadedRule.alertTypeId}`)), + const [loadedRuleType, loadedActionTypes] = await Promise.all([ + loadRuleTypes() + .then((types) => types.find((type) => type.id === loadedRule.ruleTypeId)) + .then(throwIfAbsent(`Invalid Rule Type: ${loadedRule.ruleTypeId}`)), loadActionTypes().then( throwIfIsntContained( new Set(loadedRule.actions.map((action) => action.actionTypeId)), @@ -152,12 +152,12 @@ export async function getRuleData( ), ]); - setAlertType(loadedAlertType); + setRuleType(loadedRuleType); setActionTypes(loadedActionTypes); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', + 'xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage', { defaultMessage: 'Unable to load rule: {message}', values: { @@ -169,6 +169,6 @@ export async function getRuleData( } } -const AlertDetailsRouteWithApi = withActionOperations(withBulkAlertOperations(AlertDetailsRoute)); +const RuleDetailsRouteWithApi = withActionOperations(withBulkRuleOperations(RuleDetailsRoute)); // eslint-disable-next-line import/no-default-export -export { AlertDetailsRouteWithApi as default }; +export { RuleDetailsRouteWithApi as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx similarity index 71% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx index e0b2ac6c2e6c83..d27522e74b6fb6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx @@ -9,51 +9,51 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { ToastsApi } from 'kibana/public'; -import { AlertsRoute, getAlertSummary } from './alerts_route'; -import { Rule, AlertSummary, RuleType } from '../../../../types'; +import { RuleRoute, getRuleSummary } from './rule_route'; +import { Rule, RuleSummary, RuleType } from '../../../../types'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; jest.mock('../../../../common/lib/kibana'); const fakeNow = new Date('2020-02-09T23:15:41.941Z'); const fake2MinutesAgo = new Date('2020-02-09T23:13:41.941Z'); -describe('alerts_summary_route', () => { +describe('rules_summary_route', () => { it('render a loader while fetching data', () => { const rule = mockRule(); const ruleType = mockRuleType(); expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); }); -describe('getAlertState useEffect handler', () => { +describe('getRuleState useEffect handler', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('fetches alert summary', async () => { + it('fetches rule summary', async () => { const rule = mockRule(); - const alertSummary = mockAlertSummary(); - const { loadAlertSummary } = mockApis(); - const { setAlertSummary } = mockStateSetter(); + const ruleSummary = mockRuleSummary(); + const { loadRuleSummary } = mockApis(); + const { setRuleSummary } = mockStateSetter(); - loadAlertSummary.mockImplementationOnce(async () => alertSummary); + loadRuleSummary.mockImplementationOnce(async () => ruleSummary); const toastNotifications = { addDanger: jest.fn(), } as unknown as ToastsApi; - await getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toastNotifications); + await getRuleSummary(rule.id, loadRuleSummary, setRuleSummary, toastNotifications); - expect(loadAlertSummary).toHaveBeenCalledWith(rule.id, undefined); - expect(setAlertSummary).toHaveBeenCalledWith(alertSummary); + expect(loadRuleSummary).toHaveBeenCalledWith(rule.id, undefined); + expect(setRuleSummary).toHaveBeenCalledWith(ruleSummary); }); - it('displays an error if the alert summary isnt found', async () => { + it('displays an error if the rule summary isnt found', async () => { const connectorType = { id: '.server-log', name: 'Server log', @@ -70,34 +70,34 @@ describe('getAlertState useEffect handler', () => { ], }); - const { loadAlertSummary } = mockApis(); - const { setAlertSummary } = mockStateSetter(); + const { loadRuleSummary } = mockApis(); + const { setRuleSummary } = mockStateSetter(); - loadAlertSummary.mockImplementation(async () => { + loadRuleSummary.mockImplementation(async () => { throw new Error('OMG'); }); const toastNotifications = { addDanger: jest.fn(), } as unknown as ToastsApi; - await getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toastNotifications); + await getRuleSummary(rule.id, loadRuleSummary, setRuleSummary, toastNotifications); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: 'Unable to load alerts: OMG', + title: 'Unable to load rules: OMG', }); }); }); function mockApis() { return { - loadAlertSummary: jest.fn(), + loadRuleSummary: jest.fn(), requestRefresh: jest.fn(), }; } function mockStateSetter() { return { - setAlertSummary: jest.fn(), + setRuleSummary: jest.fn(), }; } @@ -107,7 +107,7 @@ function mockRule(overloads: Partial = {}): Rule { enabled: true, name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], @@ -142,15 +142,15 @@ function mockRuleType(overloads: Partial = {}): RuleType { defaultActionGroupId: 'default', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, authorizedConsumers: {}, - producer: 'alerts', + producer: 'rules', minimumLicenseRequired: 'basic', enabledInLicense: true, ...overloads, }; } -function mockAlertSummary(overloads: Partial = {}): any { - const summary: AlertSummary = { +function mockRuleSummary(overloads: Partial = {}): any { + const summary: RuleSummary = { id: 'rule-id', name: 'rule-name', tags: ['tag-1', 'tag-2'], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx similarity index 56% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx index 07e45c8d2b2d06..393cdc404db9e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.tsx @@ -8,79 +8,79 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { Rule, AlertSummary, RuleType } from '../../../../types'; +import { Rule, RuleSummary, RuleType } from '../../../../types'; import { - ComponentOpts as AlertApis, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; -import { AlertsWithApi as Alerts } from './alerts'; + ComponentOpts as RuleApis, + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; +import { RuleWithApi as Rules } from './rule'; import { useKibana } from '../../../../common/lib/kibana'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; -type WithAlertSummaryProps = { +type WithRuleSummaryProps = { rule: Rule; ruleType: RuleType; readOnly: boolean; requestRefresh: () => Promise; refreshToken?: number; -} & Pick; +} & Pick; -export const AlertsRoute: React.FunctionComponent = ({ +export const RuleRoute: React.FunctionComponent = ({ rule, ruleType, readOnly, requestRefresh, - loadAlertSummary: loadAlertSummary, + loadRuleSummary: loadRuleSummary, refreshToken, }) => { const { notifications: { toasts }, } = useKibana().services; - const [alertSummary, setAlertSummary] = useState(null); + const [ruleSummary, setRuleSummary] = useState(null); const [numberOfExecutions, setNumberOfExecutions] = useState(60); const [isLoadingChart, setIsLoadingChart] = useState(true); const ruleID = useRef(null); const refreshTokenRef = useRef(refreshToken); - const getAlertSummaryWithLoadingState = useCallback( + const getRuleSummaryWithLoadingState = useCallback( async (executions: number = numberOfExecutions) => { setIsLoadingChart(true); - await getAlertSummary(ruleID.current!, loadAlertSummary, setAlertSummary, toasts, executions); + await getRuleSummary(ruleID.current!, loadRuleSummary, setRuleSummary, toasts, executions); setIsLoadingChart(false); }, - [setIsLoadingChart, ruleID, loadAlertSummary, setAlertSummary, toasts, numberOfExecutions] + [setIsLoadingChart, ruleID, loadRuleSummary, setRuleSummary, toasts, numberOfExecutions] ); useEffect(() => { if (ruleID.current !== rule.id) { ruleID.current = rule.id; - getAlertSummaryWithLoadingState(); + getRuleSummaryWithLoadingState(); } - }, [rule, ruleID, getAlertSummaryWithLoadingState]); + }, [rule, ruleID, getRuleSummaryWithLoadingState]); useEffect(() => { if (refreshTokenRef.current !== refreshToken) { refreshTokenRef.current = refreshToken; - getAlertSummaryWithLoadingState(); + getRuleSummaryWithLoadingState(); } - }, [refreshToken, refreshTokenRef, getAlertSummaryWithLoadingState]); + }, [refreshToken, refreshTokenRef, getRuleSummaryWithLoadingState]); const onChangeDuration = useCallback( (executions: number) => { setNumberOfExecutions(executions); - getAlertSummaryWithLoadingState(executions); + getRuleSummaryWithLoadingState(executions); }, - [getAlertSummaryWithLoadingState] + [getRuleSummaryWithLoadingState] ); - return alertSummary ? ( - = ({ ); }; -export async function getAlertSummary( +export async function getRuleSummary( ruleId: string, - loadAlertSummary: AlertApis['loadAlertSummary'], - setAlertSummary: React.Dispatch>, + loadRuleSummary: RuleApis['loadRuleSummary'], + setRuleSummary: React.Dispatch>, toasts: Pick, executionDuration?: number ) { try { - const loadedSummary = await loadAlertSummary(ruleId, executionDuration); - setAlertSummary(loadedSummary); + const loadedSummary = await loadRuleSummary(ruleId, executionDuration); + setRuleSummary(loadedSummary); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertsMessage', + 'xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage', { - defaultMessage: 'Unable to load alerts: {message}', + defaultMessage: 'Unable to load rules: {message}', values: { message: e.message, }, @@ -115,4 +115,4 @@ export async function getAlertSummary( } } -export const AlertsRouteWithApi = withBulkAlertOperations(AlertsRoute); +export const RuleRouteWithApi = withBulkRuleOperations(RuleRoute); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.test.tsx similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.test.tsx index d177e94b8fd745..ee29facf605795 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.test.tsx @@ -16,30 +16,30 @@ import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../lib/capabilities', () => ({ - hasSaveAlertsCapability: jest.fn(() => true), + hasSaveRulesCapability: jest.fn(() => true), })); describe('view in app', () => { - describe('link to the app that created the alert', () => { + describe('link to the app that created the rule', () => { it('is disabled when there is no navigation', async () => { - const alert = mockAlert(); + const rule = mockRule(); const { alerting } = useKibana().services; let component: ReactWrapper; await act(async () => { // use mount as we need useEffect to run - component = mount(); + component = mount(); await waitForUseEffect(); expect(component!.find('button').prop('disabled')).toBe(true); expect(component!.text()).toBe('View in app'); - expect(alerting!.getNavigation).toBeCalledWith(alert.id); + expect(alerting!.getNavigation).toBeCalledWith(rule.id); }); }); it('enabled when there is navigation', async () => { - const alert = mockAlert({ id: 'alert-with-nav', consumer: 'siem' }); + const rule = mockRule({ id: 'rule-with-nav', consumer: 'siem' }); const { application: { navigateToApp }, } = useKibana().services; @@ -47,7 +47,7 @@ describe('view in app', () => { let component: ReactWrapper; act(async () => { // use mount as we need useEffect to run - component = mount(); + component = mount(); await waitForUseEffect(); @@ -57,7 +57,7 @@ describe('view in app', () => { currentTarget: {}, } as React.MouseEvent<{}, MouseEvent>); - expect(navigateToApp).toBeCalledWith('siem', '/alert'); + expect(navigateToApp).toBeCalledWith('siem', '/rule'); }); }); }); @@ -69,13 +69,13 @@ function waitForUseEffect() { }); } -function mockAlert(overloads: Partial = {}): Rule { +function mockRule(overloads: Partial = {}): Rule { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.tsx similarity index 53% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.tsx index df80be989aaa77..85b9bcd79806fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/view_in_app.tsx @@ -13,59 +13,59 @@ import { fromNullable, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; import { - AlertNavigation, - AlertStateNavigation, - AlertUrlNavigation, + AlertNavigation as RuleNavigation, + AlertStateNavigation as RuleStateNavigation, + AlertUrlNavigation as RuleUrlNavigation, } from '../../../../../../alerting/common'; import { Rule } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; export interface ViewInAppProps { - alert: Rule; + rule: Rule; } const NO_NAVIGATION = false; -type AlertNavigationLoadingState = AlertNavigation | false | null; +type RuleNavigationLoadingState = RuleNavigation | false | null; -export const ViewInApp: React.FunctionComponent = ({ alert }) => { +export const ViewInApp: React.FunctionComponent = ({ rule }) => { const { application: { navigateToApp }, alerting: maybeAlerting, } = useKibana().services; - const [alertNavigation, setAlertNavigation] = useState(null); + const [ruleNavigation, setRuleNavigation] = useState(null); useEffect(() => { pipe( fromNullable(maybeAlerting), fold( /** - * If the alerting plugin is disabled, + * If the ruleing plugin is disabled, * navigation isn't supported */ - () => setAlertNavigation(NO_NAVIGATION), - (alerting) => { - return alerting - .getNavigation(alert.id) - .then((nav) => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION))) + () => setRuleNavigation(NO_NAVIGATION), + (ruleing) => { + return ruleing + .getNavigation(rule.id) + .then((nav) => (nav ? setRuleNavigation(nav) : setRuleNavigation(NO_NAVIGATION))) .catch(() => { - setAlertNavigation(NO_NAVIGATION); + setRuleNavigation(NO_NAVIGATION); }); } ) ); - }, [alert.id, maybeAlerting]); + }, [rule.id, maybeAlerting]); return ( @@ -73,22 +73,22 @@ export const ViewInApp: React.FunctionComponent = ({ alert }) => }; function hasNavigation( - alertNavigation: AlertNavigationLoadingState -): alertNavigation is AlertStateNavigation | AlertUrlNavigation { - return alertNavigation - ? alertNavigation.hasOwnProperty('state') || alertNavigation.hasOwnProperty('path') + ruleNavigation: RuleNavigationLoadingState +): ruleNavigation is RuleStateNavigation | RuleUrlNavigation { + return ruleNavigation + ? ruleNavigation.hasOwnProperty('state') || ruleNavigation.hasOwnProperty('path') : NO_NAVIGATION; } function getNavigationHandler( - alertNavigation: AlertNavigationLoadingState, - alert: Rule, + ruleNavigation: RuleNavigationLoadingState, + rule: Rule, navigateToApp: CoreStart['application']['navigateToApp'] ): object { - return hasNavigation(alertNavigation) + return hasNavigation(ruleNavigation) ? { onClick: () => { - navigateToApp(alert.consumer, alertNavigation); + navigateToApp(rule.consumer, ruleNavigation); }, } : {}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx similarity index 69% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx index e3de4804dc08f7..1b3ad3473b7ecd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx @@ -15,11 +15,11 @@ interface Props { onCancel: () => void; } -export const ConfirmAlertClose: React.FC = ({ onConfirm, onCancel }) => { +export const ConfirmRuleClose: React.FC = ({ onConfirm, onCancel }) => { return ( = ({ onConfirm, onCancel }) => { onCancel={onCancel} onConfirm={onConfirm} confirmButtonText={i18n.translate( - 'xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseConfirmButtonText', + 'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText', { defaultMessage: 'Discard changes', } )} cancelButtonText={i18n.translate( - 'xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseCancelButtonText', + 'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText', { defaultMessage: 'Cancel', } )} defaultFocusedButton="confirm" - data-test-subj="confirmAlertCloseModal" + data-test-subj="confirmRuleCloseModal" >

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx similarity index 68% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx index ebc2407774bdab..a3359374f2d109 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx @@ -15,11 +15,11 @@ interface Props { onCancel: () => void; } -export const ConfirmAlertSave: React.FC = ({ onConfirm, onCancel }) => { +export const ConfirmRuleSave: React.FC = ({ onConfirm, onCancel }) => { return ( = ({ onConfirm, onCancel }) => { onCancel={onCancel} onConfirm={onConfirm} confirmButtonText={i18n.translate( - 'xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveConfirmButtonText', + 'xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText', { defaultMessage: 'Save rule', } )} cancelButtonText={i18n.translate( - 'xpack.triggersActionsUI.sections.confirmAlertSave.confirmAlertSaveCancelButtonText', + 'xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText', { defaultMessage: 'Cancel', } )} defaultFocusedButton="confirm" - data-test-subj="confirmAlertSaveModal" + data-test-subj="confirmRuleSaveModal" >

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts new file mode 100644 index 00000000000000..17f54d297b9be4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InitialRule } from './rule_reducer'; +import { hasRuleChanged } from './has_rule_changed'; + +function createRule(overrides = {}): InitialRule { + return { + params: {}, + consumer: 'test', + ruleTypeId: 'test', + schedule: { + interval: '1m', + }, + actions: [], + tags: [], + notifyWhen: 'onActionGroupChange', + ...overrides, + }; +} + +test('should return false for same rule', () => { + const a = createRule(); + expect(hasRuleChanged(a, a, true)).toEqual(false); +}); + +test('should return true for different rule', () => { + const a = createRule(); + const b = createRule({ ruleTypeId: 'differentTest' }); + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); + +test('should correctly compare name field', () => { + // name field doesn't exist initially + const a = createRule(); + // set name to actual value + const b = createRule({ name: 'myRule' }); + // set name to different value + const c = createRule({ name: 'anotherRule' }); + // set name to various empty/null/undefined states + const d = createRule({ name: '' }); + const e = createRule({ name: undefined }); + const f = createRule({ name: null }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); + expect(hasRuleChanged(a, c, true)).toEqual(true); + expect(hasRuleChanged(a, d, true)).toEqual(false); + expect(hasRuleChanged(a, e, true)).toEqual(false); + expect(hasRuleChanged(a, f, true)).toEqual(false); + + expect(hasRuleChanged(b, c, true)).toEqual(true); + expect(hasRuleChanged(b, d, true)).toEqual(true); + expect(hasRuleChanged(b, e, true)).toEqual(true); + expect(hasRuleChanged(b, f, true)).toEqual(true); + + expect(hasRuleChanged(c, d, true)).toEqual(true); + expect(hasRuleChanged(c, e, true)).toEqual(true); + expect(hasRuleChanged(c, f, true)).toEqual(true); + + expect(hasRuleChanged(d, e, true)).toEqual(false); + expect(hasRuleChanged(d, f, true)).toEqual(false); +}); + +test('should correctly compare ruleTypeId field', () => { + const a = createRule(); + + // set ruleTypeId to different value + const b = createRule({ ruleTypeId: 'myRuleId' }); + // set ruleTypeId to various empty/null/undefined states + const c = createRule({ ruleTypeId: '' }); + const d = createRule({ ruleTypeId: undefined }); + const e = createRule({ ruleTypeId: null }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); + expect(hasRuleChanged(a, c, true)).toEqual(true); + expect(hasRuleChanged(a, d, true)).toEqual(true); + expect(hasRuleChanged(a, e, true)).toEqual(true); + + expect(hasRuleChanged(b, c, true)).toEqual(true); + expect(hasRuleChanged(b, d, true)).toEqual(true); + expect(hasRuleChanged(b, e, true)).toEqual(true); + + expect(hasRuleChanged(c, d, true)).toEqual(false); + expect(hasRuleChanged(c, e, true)).toEqual(false); + expect(hasRuleChanged(d, e, true)).toEqual(false); +}); + +test('should correctly compare throttle field', () => { + // throttle field doesn't exist initially + const a = createRule(); + // set throttle to actual value + const b = createRule({ throttle: '1m' }); + // set throttle to different value + const c = createRule({ throttle: '1h' }); + // set throttle to various empty/null/undefined states + const d = createRule({ throttle: '' }); + const e = createRule({ throttle: undefined }); + const f = createRule({ throttle: null }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); + expect(hasRuleChanged(a, c, true)).toEqual(true); + expect(hasRuleChanged(a, d, true)).toEqual(false); + expect(hasRuleChanged(a, e, true)).toEqual(false); + expect(hasRuleChanged(a, f, true)).toEqual(false); + + expect(hasRuleChanged(b, c, true)).toEqual(true); + expect(hasRuleChanged(b, d, true)).toEqual(true); + expect(hasRuleChanged(b, e, true)).toEqual(true); + expect(hasRuleChanged(b, f, true)).toEqual(true); + + expect(hasRuleChanged(c, d, true)).toEqual(true); + expect(hasRuleChanged(c, e, true)).toEqual(true); + expect(hasRuleChanged(c, f, true)).toEqual(true); + + expect(hasRuleChanged(d, e, true)).toEqual(false); + expect(hasRuleChanged(d, f, true)).toEqual(false); +}); + +test('should correctly compare tags field', () => { + const a = createRule(); + const b = createRule({ tags: ['first'] }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); + +test('should correctly compare schedule field', () => { + const a = createRule(); + const b = createRule({ schedule: { interval: '3h' } }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); + +test('should correctly compare actions field', () => { + const a = createRule(); + const b = createRule({ + actions: [{ actionTypeId: 'action', group: 'group', id: 'actionId', params: {} }], + }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); + +test('should skip comparing params field if compareParams=false', () => { + const a = createRule(); + const b = createRule({ params: { newParam: 'value' } }); + + expect(hasRuleChanged(a, b, false)).toEqual(false); +}); + +test('should correctly compare params field if compareParams=true', () => { + const a = createRule(); + const b = createRule({ params: { newParam: 'value' } }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); + +test('should correctly compare notifyWhen field', () => { + const a = createRule(); + const b = createRule({ notifyWhen: 'onActiveAlert' }); + + expect(hasRuleChanged(a, b, true)).toEqual(true); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.ts rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts index bcf34f6d4ad0f2..a79e820e2cfd83 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/has_alert_changed.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts @@ -8,20 +8,20 @@ import deepEqual from 'fast-deep-equal'; import { pick } from 'lodash'; import { RuleTypeParams } from '../../../types'; -import { InitialAlert } from './alert_reducer'; +import { InitialRule } from './rule_reducer'; const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions', 'notifyWhen']; -function getNonNullCompareFields(alert: InitialAlert) { - const { name, alertTypeId, throttle } = alert; +function getNonNullCompareFields(rule: InitialRule) { + const { name, ruleTypeId, throttle } = rule; return { ...(!!(name && name.length > 0) ? { name } : {}), - ...(!!(alertTypeId && alertTypeId.length > 0) ? { alertTypeId } : {}), + ...(!!(ruleTypeId && ruleTypeId.length > 0) ? { ruleTypeId } : {}), ...(!!(throttle && throttle.length > 0) ? { throttle } : {}), }; } -export function hasAlertChanged(a: InitialAlert, b: InitialAlert, compareParams: boolean) { +export function hasRuleChanged(a: InitialRule, b: InitialRule, compareParams: boolean) { // Deep compare these fields let objectsAreEqual = deepEqual(pick(a, DEEP_COMPARE_FIELDS), pick(b, DEEP_COMPARE_FIELDS)); if (compareParams) { @@ -36,6 +36,6 @@ export function hasAlertChanged(a: InitialAlert, b: InitialAlert, compareParams: return !objectsAreEqual || !nonNullCompareFieldsAreEqual; } -export function haveAlertParamsChanged(a: RuleTypeParams, b: RuleTypeParams) { +export function haveRuleParamsChanged(a: RuleTypeParams, b: RuleTypeParams) { return !deepEqual(a, b); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx similarity index 67% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx index ac2648eea3e897..dfab594febf103 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx @@ -8,5 +8,5 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; -export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add'))); -export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit'))); +export const RuleAdd = suspendedComponentWithProps(lazy(() => import('./rule_add'))); +export const RuleEdit = suspendedComponentWithProps(lazy(() => import('./rule_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index 7e45dd8ac636ac..01e459011f26bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -12,13 +12,13 @@ import { act } from 'react-dom/test-utils'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import AlertAdd from './alert_add'; -import { createAlert } from '../../lib/alert_api'; +import RuleAdd from './rule_add'; +import { createRule } from '../../lib/rule_api'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { Rule, - AlertAddProps, - AlertFlyoutCloseReason, + RuleAddProps, + RuleFlyoutCloseReason, ConnectorValidationResult, GenericValidationResult, ValidationResult, @@ -30,9 +30,9 @@ import { useKibana } from '../../../common/lib/kibana'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/alert_api', () => ({ - loadAlertTypes: jest.fn(), - createAlert: jest.fn(), +jest.mock('../../lib/rule_api', () => ({ + loadRuleTypes: jest.fn(), + createRule: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, @@ -40,7 +40,7 @@ jest.mock('../../lib/alert_api', () => ({ })); jest.mock('../../../common/lib/health_api', () => ({ - triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), + triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), })); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -52,26 +52,26 @@ export const TestExpression: FunctionComponent = () => { ); }; -describe('alert_add', () => { +describe('rule_add', () => { let wrapper: ReactWrapper; async function setup( initialValues?: Partial, - onClose: AlertAddProps['onClose'] = jest.fn(), + onClose: RuleAddProps['onClose'] = jest.fn(), defaultScheduleInterval?: string ) { const mocks = coreMock.createSetup(); - const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); - const alertTypes = [ + const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); + const ruleTypes = [ { - id: 'my-alert-type', + id: 'my-rule-type', name: 'Test', actionGroups: [ { @@ -95,7 +95,7 @@ describe('alert_add', () => { }, }, ]; - loadAlertTypes.mockResolvedValue(alertTypes); + loadRuleTypes.mockResolvedValue(ruleTypes); const [ { application: { capabilities }, @@ -104,7 +104,7 @@ describe('alert_add', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.application.capabilities = { ...capabilities, - alerts: { + rules: { show: true, save: true, delete: true, @@ -117,7 +117,7 @@ describe('alert_add', () => { }); const ruleType = { - id: 'my-alert-type', + id: 'my-rule-type', iconClass: 'test', description: 'test', documentationUrl: null, @@ -150,7 +150,7 @@ describe('alert_add', () => { actionTypeRegistry.has.mockReturnValue(true); wrapper = mountWithIntl( - { }); } - it('renders alert add flyout', async () => { + it('renders rule add flyout', async () => { const onClose = jest.fn(); await setup({}, onClose); - expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="addRuleFlyoutTitle"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy(); - wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); + wrapper.find('[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); - expect(wrapper.find('input#alertName').props().value).toBe(''); + expect(wrapper.find('input#ruleName').props().value).toBe(''); expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe(''); expect(wrapper.find('.euiSelect').first().props().value).toBe('m'); - wrapper.find('[data-test-subj="cancelSaveAlertButton"]').first().simulate('click'); - expect(onClose).toHaveBeenCalledWith(AlertFlyoutCloseReason.CANCELED); + wrapper.find('[data-test-subj="cancelSaveRuleButton"]').first().simulate('click'); + expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.CANCELED); }); - it('renders alert add flyout with initial values', async () => { + it('renders rule add flyout with initial values', async () => { const onClose = jest.fn(); await setup( { - name: 'Simple status alert', + name: 'Simple status rule', tags: ['uptime', 'logs'], schedule: { interval: '1h', @@ -202,23 +202,23 @@ describe('alert_add', () => { onClose ); - expect(wrapper.find('input#alertName').props().value).toBe('Simple status alert'); + expect(wrapper.find('input#ruleName').props().value).toBe('Simple status rule'); expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('uptimelogs'); expect(wrapper.find('.euiSelect').first().props().value).toBe('h'); }); - it('emit an onClose event when the alert is saved', async () => { + it('emit an onClose event when the rule is saved', async () => { const onClose = jest.fn(); - const alert = mockAlert(); + const rule = mockRule(); - (createAlert as jest.MockedFunction).mockResolvedValue(alert); + (createRule as jest.MockedFunction).mockResolvedValue(rule); await setup( { - name: 'Simple status alert', - alertTypeId: 'my-alert-type', + name: 'Simple status rule', + ruleTypeId: 'my-rule-type', tags: ['uptime', 'logs'], schedule: { interval: '1h', @@ -227,7 +227,7 @@ describe('alert_add', () => { onClose ); - wrapper.find('[data-test-subj="saveAlertButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="saveRuleButton"]').first().simulate('click'); // Wait for handlers to fire await act(async () => { @@ -235,11 +235,11 @@ describe('alert_add', () => { wrapper.update(); }); - expect(onClose).toHaveBeenCalledWith(AlertFlyoutCloseReason.SAVED); + expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED); }); it('should enforce any default inteval', async () => { - await setup({ alertTypeId: 'my-alert-type' }, jest.fn(), '3h'); + await setup({ ruleTypeId: 'my-rule-type' }, jest.fn(), '3h'); // Wait for handlers to fire await act(async () => { @@ -258,13 +258,13 @@ describe('alert_add', () => { }); }); -function mockAlert(overloads: Partial = {}): Rule { +function mockRule(overloads: Partial = {}): Rule { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx new file mode 100644 index 00000000000000..98b7bfd36452cf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -0,0 +1,303 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { useReducer, useMemo, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { + Rule, + RuleTypeParams, + RuleUpdates, + RuleFlyoutCloseReason, + IErrorObject, + RuleAddProps, + RuleTypeIndex, +} from '../../../types'; +import { RuleForm } from './rule_form'; +import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; +import { ruleReducer, InitialRule, InitialRuleReducer } from './rule_reducer'; +import { createRule, loadRuleTypes } from '../../lib/rule_api'; +import { HealthCheck } from '../../components/health_check'; +import { ConfirmRuleSave } from './confirm_rule_save'; +import { ConfirmRuleClose } from './confirm_rule_close'; +import { hasShowActionsCapability } from '../../lib/capabilities'; +import RuleAddFooter from './rule_add_footer'; +import { HealthContextProvider } from '../../context/health_context'; +import { useKibana } from '../../../common/lib/kibana'; +import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed'; +import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; +import { DEFAULT_ALERT_INTERVAL } from '../../constants'; + +const RuleAdd = ({ + consumer, + ruleTypeRegistry, + actionTypeRegistry, + onClose, + canChangeTrigger, + ruleTypeId, + initialValues, + reloadRules, + onSave, + metadata, + ...props +}: RuleAddProps) => { + const onSaveHandler = onSave ?? reloadRules; + + const initialRule: InitialRule = useMemo(() => { + return { + params: {}, + consumer, + ruleTypeId, + schedule: { + interval: DEFAULT_ALERT_INTERVAL, + }, + actions: [], + tags: [], + notifyWhen: 'onActionGroupChange', + ...(initialValues ? initialValues : {}), + }; + }, [ruleTypeId, consumer, initialValues]); + + const [{ rule }, dispatch] = useReducer(ruleReducer as InitialRuleReducer, { + rule: initialRule, + }); + const [initialRuleParams, setInitialRuleParams] = useState({}); + const [isSaving, setIsSaving] = useState(false); + const [isConfirmRuleSaveModalOpen, setIsConfirmRuleSaveModalOpen] = useState(false); + const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState(false); + const [ruleTypeIndex, setRuleTypeIndex] = useState( + props.ruleTypeIndex + ); + const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); + + const setRule = (value: InitialRule) => { + dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); + }; + + const setRuleProperty = (key: Key, value: Rule[Key] | null) => { + dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); + }; + + const { + http, + notifications: { toasts }, + application: { capabilities }, + } = useKibana().services; + + const canShowActions = hasShowActionsCapability(capabilities); + + useEffect(() => { + if (ruleTypeId) { + setRuleProperty('ruleTypeId', ruleTypeId); + } + }, [ruleTypeId]); + + useEffect(() => { + if (!props.ruleTypeIndex) { + (async () => { + const ruleTypes = await loadRuleTypes({ http }); + const index: RuleTypeIndex = new Map(); + for (const ruleType of ruleTypes) { + index.set(ruleType.id, ruleType); + } + setRuleTypeIndex(index); + })(); + } + }, [props.ruleTypeIndex, http]); + + useEffect(() => { + if (isEmpty(rule.params) && !isEmpty(initialRuleParams)) { + // rule params are explicitly cleared when the rule type is cleared. + // clear the "initial" params in order to capture the + // default when a new rule type is selected + setInitialRuleParams({}); + } else if (isEmpty(initialRuleParams)) { + // captures the first change to the rule params, + // when consumers set a default value for the rule params + setInitialRuleParams(rule.params); + } + }, [rule.params, initialRuleParams]); + + const [ruleActionsErrors, setRuleActionsErrors] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + (async () => { + setIsLoading(true); + const res = await getRuleActionErrors(rule as Rule, actionTypeRegistry); + setIsLoading(false); + setRuleActionsErrors([...res]); + })(); + }, [rule, actionTypeRegistry]); + + useEffect(() => { + if (rule.ruleTypeId && ruleTypeIndex) { + const type = ruleTypeIndex.get(rule.ruleTypeId); + if (type?.defaultScheduleInterval && !changedFromDefaultInterval) { + setRuleProperty('schedule', { interval: type.defaultScheduleInterval }); + } + } + }, [rule.ruleTypeId, ruleTypeIndex, rule.schedule.interval, changedFromDefaultInterval]); + + useEffect(() => { + if (rule.schedule.interval !== DEFAULT_ALERT_INTERVAL && !changedFromDefaultInterval) { + setChangedFromDefaultInterval(true); + } + }, [rule.schedule.interval, changedFromDefaultInterval]); + + const checkForChangesAndCloseFlyout = () => { + if ( + hasRuleChanged(rule, initialRule, false) || + haveRuleParamsChanged(rule.params, initialRuleParams) + ) { + setIsConfirmRuleCloseModalOpen(true); + } else { + onClose(RuleFlyoutCloseReason.CANCELED); + } + }; + + const saveRuleAndCloseFlyout = async () => { + const savedRule = await onSaveRule(); + setIsSaving(false); + if (savedRule) { + onClose(RuleFlyoutCloseReason.SAVED); + if (onSaveHandler) { + onSaveHandler(); + } + } + }; + + const ruleType = rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null; + + const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = getRuleErrors( + rule as Rule, + ruleType, + rule.ruleTypeId ? ruleTypeIndex?.get(rule.ruleTypeId) : undefined + ); + + // Confirm before saving if user is able to add actions but hasn't added any to this rule + const shouldConfirmSave = canShowActions && rule.actions?.length === 0; + + async function onSaveRule(): Promise { + try { + const newRule = await createRule({ http, rule: rule as RuleUpdates }); + toasts.addSuccess( + i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText', { + defaultMessage: 'Created rule "{ruleName}"', + values: { + ruleName: newRule.name, + }, + }) + ); + return newRule; + } catch (errorRes) { + toasts.addDanger( + errorRes.body?.message ?? + i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText', { + defaultMessage: 'Cannot create rule.', + }) + ); + } + } + + return ( + + + + +

+ +

+
+
+ + + + + + { + setIsSaving(true); + if (isLoading || !isValidRule(rule, ruleErrors, ruleActionsErrors)) { + setRule( + getRuleWithInvalidatedFields( + rule as Rule, + ruleParamsErrors, + ruleBaseErrors, + ruleActionsErrors + ) + ); + setIsSaving(false); + return; + } + if (shouldConfirmSave) { + setIsConfirmRuleSaveModalOpen(true); + } else { + await saveRuleAndCloseFlyout(); + } + }} + onCancel={checkForChangesAndCloseFlyout} + /> + + + {isConfirmRuleSaveModalOpen && ( + { + setIsConfirmRuleSaveModalOpen(false); + await saveRuleAndCloseFlyout(); + }} + onCancel={() => { + setIsSaving(false); + setIsConfirmRuleSaveModalOpen(false); + }} + /> + )} + {isConfirmRuleCloseModalOpen && ( + { + setIsConfirmRuleCloseModalOpen(false); + onClose(RuleFlyoutCloseReason.CANCELED); + }} + onCancel={() => { + setIsConfirmRuleCloseModalOpen(false); + }} + /> + )} +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleAdd as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx index a0f0ec66d2346d..f2d035b2112aa8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx @@ -19,27 +19,27 @@ import { import { i18n } from '@kbn/i18n'; import { useHealthContext } from '../../context/health_context'; -interface AlertAddFooterProps { +interface RuleAddFooterProps { isSaving: boolean; isFormLoading: boolean; onSave: () => void; onCancel: () => void; } -export const AlertAddFooter = ({ +export const RuleAddFooter = ({ isSaving, onSave, onCancel, isFormLoading, -}: AlertAddFooterProps) => { +}: RuleAddFooterProps) => { const { loadingHealthCheck } = useHealthContext(); return ( - - {i18n.translate('xpack.triggersActionsUI.sections.alertAddFooter.cancelButtonLabel', { + + {i18n.translate('xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel', { defaultMessage: 'Cancel', })} @@ -56,7 +56,7 @@ export const AlertAddFooter = ({ @@ -75,4 +75,4 @@ export const AlertAddFooter = ({ }; // eslint-disable-next-line import/no-default-export -export { AlertAddFooter as default }; +export { RuleAddFooter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx similarity index 93% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx index 07f7b1475b92fa..bde4596a518fba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; -import { AlertConditions, ActionGroupWithCondition } from './alert_conditions'; +import { RuleConditions, ActionGroupWithCondition } from './rule_conditions'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiTitle, @@ -19,7 +19,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; -describe('alert_conditions', () => { +describe('rule_conditions', () => { async function setup(element: React.ReactElement): Promise> { const wrapper = mountWithIntl(element); @@ -34,24 +34,23 @@ describe('alert_conditions', () => { it('renders with custom headline', async () => { const wrapper = await setup( - ); expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot( - `"xpack.triggersActionsUI.sections.alertForm.conditions.title"` + `"xpack.triggersActionsUI.sections.ruleForm.conditions.title"` ); expect( wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage') ).toMatchInlineSnapshot(`"Conditions:"`); - expect(wrapper.find('[data-test-subj="alertConditionsHeadline"]').get(0)) - .toMatchInlineSnapshot(` + expect(wrapper.find('[data-test-subj="ruleConditionsHeadline"]').get(0)).toMatchInlineSnapshot(` Set different threshold with their own status @@ -80,13 +79,13 @@ describe('alert_conditions', () => { }; const wrapper = await setup( - - + ); expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) @@ -126,7 +125,7 @@ describe('alert_conditions', () => { }; const wrapper = await setup( - { ]} > - + ); expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) @@ -178,7 +177,7 @@ describe('alert_conditions', () => { }; const wrapper = await setup( - { onInitializeConditionsFor={onInitializeConditionsFor} > - + ); expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` @@ -246,13 +245,13 @@ describe('alert_conditions', () => { }; await setup( - - + ); expect(callbackProp).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx similarity index 87% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx index 217f99c62a4f97..94acf30ece4ea3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx @@ -30,7 +30,7 @@ export type ActionGroupWithCondition< } ); -export interface AlertConditionsProps { +export interface RuleConditionsProps { headline?: string; actionGroups: Array>; onInitializeConditionsFor?: ( @@ -42,14 +42,14 @@ export interface AlertConditionsProps({ +export const RuleConditions = ({ headline, actionGroups, onInitializeConditionsFor, onResetConditionsFor, includeBuiltInActionGroups = false, children, -}: PropsWithChildren>) => { +}: PropsWithChildren>) => { const [withConditions, withoutConditions] = partition( includeBuiltInActionGroups ? actionGroups @@ -63,16 +63,16 @@ export const AlertConditions = -
+
{headline && ( - + {headline} @@ -101,7 +101,7 @@ export const AlertConditions = @@ -126,4 +126,4 @@ export const AlertConditions = { +describe('rule_conditions_group', () => { async function setup(element: React.ReactElement): Promise> { const wrapper = mountWithIntl(element); @@ -28,14 +28,14 @@ describe('alert_conditions_group', () => { it('renders with actionGroup name as label', async () => { const InnerComponent = () =>
{'inner component'}
; const wrapper = await setup( - - + ); expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(` @@ -58,7 +58,7 @@ describe('alert_conditions_group', () => { it('renders a reset button when onResetConditionsFor is specified', async () => { const onResetConditionsFor = jest.fn(); const wrapper = await setup( - { onResetConditionsFor={onResetConditionsFor} >
{'inner component'}
-
+ ); expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`); @@ -82,7 +82,7 @@ describe('alert_conditions_group', () => { it('shouldnt render a reset button when isRequired is true', async () => { const onResetConditionsFor = jest.fn(); const wrapper = await setup( - { onResetConditionsFor={onResetConditionsFor} >
{'inner component'}
-
+ ); expect(wrapper.find(EuiButtonIcon).length).toEqual(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx index dd0a7df38eb629..0b2d4d5fd26ced 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx @@ -8,18 +8,18 @@ import React, { PropsWithChildren } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; -import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions'; +import { RuleConditionsProps, ActionGroupWithCondition } from './rule_conditions'; -export type AlertConditionsGroupProps = { +export type RuleConditionsGroupProps = { actionGroup?: ActionGroupWithCondition; -} & Pick, 'onResetConditionsFor'>; +} & Pick, 'onResetConditionsFor'>; -export const AlertConditionsGroup = ({ +export const RuleConditionsGroup = ({ actionGroup, onResetConditionsFor, children, ...otherProps -}: PropsWithChildren>) => { +}: PropsWithChildren>) => { if (!actionGroup) { return null; } @@ -39,7 +39,7 @@ export const AlertConditionsGroup = ({ iconType="minusInCircle" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel', + 'xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel', { defaultMessage: 'Remove', } @@ -62,4 +62,4 @@ export const AlertConditionsGroup = ({ }; // eslint-disable-next-line import/no-default-export -export { AlertConditionsGroup as default }; +export { RuleConditionsGroup as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx similarity index 75% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx index ec33756cfec4a6..bdefb730fa716e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx @@ -18,7 +18,7 @@ import { } from '../../../types'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { ReactWrapper } from 'enzyme'; -import AlertEdit from './alert_edit'; +import RuleEdit from './rule_edit'; import { useKibana } from '../../../common/lib/kibana'; import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; jest.mock('../../../common/lib/kibana'); @@ -26,37 +26,37 @@ const actionTypeRegistry = actionTypeRegistryMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const useKibanaMock = useKibana as jest.Mocked; -jest.mock('../../lib/alert_api', () => ({ - loadAlertTypes: jest.fn(), - updateAlert: jest.fn().mockRejectedValue({ body: { message: 'Fail message' } }), +jest.mock('../../lib/rule_api', () => ({ + loadRuleTypes: jest.fn(), + updateRule: jest.fn().mockRejectedValue({ body: { message: 'Fail message' } }), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, })), })); -jest.mock('./alert_errors', () => ({ - getAlertActionErrors: jest.fn().mockImplementation(() => { +jest.mock('./rule_errors', () => ({ + getRuleActionErrors: jest.fn().mockImplementation(() => { return []; }), - getAlertErrors: jest.fn().mockImplementation(() => ({ - alertParamsErrors: {}, - alertBaseErrors: {}, - alertErrors: { + getRuleErrors: jest.fn().mockImplementation(() => ({ + ruleParamsErrors: {}, + ruleBaseErrors: {}, + ruleErrors: { name: new Array(), interval: new Array(), - alertTypeId: new Array(), + ruleTypeId: new Array(), actionConnectors: new Array(), }, })), - isValidAlert: jest.fn(), + isValidRule: jest.fn(), })); jest.mock('../../../common/lib/health_api', () => ({ - triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), + triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), })); -describe('alert_edit', () => { +describe('rule_edit', () => { let wrapper: ReactWrapper; let mockedCoreSetup: ReturnType; @@ -64,7 +64,7 @@ describe('alert_edit', () => { mockedCoreSetup = coreMock.createSetup(); }); - async function setup(initialAlertFields = {}) { + async function setup(initialRuleFields = {}) { const [ { application: { capabilities }, @@ -73,7 +73,7 @@ describe('alert_edit', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.application.capabilities = { ...capabilities, - alerts: { + rules: { show: true, save: true, delete: true, @@ -81,10 +81,10 @@ describe('alert_edit', () => { }, }; - const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); - const alertTypes = [ + const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); + const ruleTypes = [ { - id: 'my-alert-type', + id: 'my-rule-type', name: 'Test', actionGroups: [ { @@ -108,7 +108,7 @@ describe('alert_edit', () => { }, ]; const ruleType = { - id: 'my-alert-type', + id: 'my-rule-type', iconClass: 'test', description: 'test', documentationUrl: null, @@ -132,8 +132,8 @@ describe('alert_edit', () => { }, actionConnectorFields: null, }); - loadAlertTypes.mockResolvedValue(alertTypes); - const alert: Rule = { + loadRuleTypes.mockResolvedValue(ruleTypes); + const rule: Rule = { id: 'ab5661e0-197e-45ee-b477-302d89193b5e', params: { aggType: 'average', @@ -144,20 +144,20 @@ describe('alert_edit', () => { window: '1s', comparator: 'between', }, - consumer: 'alerts', - alertTypeId: 'my-alert-type', + consumer: 'rules', + ruleTypeId: 'my-rule-type', enabled: false, schedule: { interval: '1m' }, actions: [ { actionTypeId: 'my-action-type', group: 'threshold met', - params: { message: 'Alert [{{ctx.metadata.name}}] has exceeded the threshold' }, + params: { message: 'Rule [{{ctx.metadata.name}}] has exceeded the threshold' }, id: '917f5d41-fbc4-4056-a8ad-ac592f7dcee2', }, ], tags: [], - name: 'test alert', + name: 'test rule', throttle: null, notifyWhen: null, apiKeyOwner: null, @@ -171,7 +171,7 @@ describe('alert_edit', () => { status: 'unknown', lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, - ...initialAlertFields, + ...initialRuleFields, }; actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); actionTypeRegistry.has.mockReturnValue(true); @@ -182,9 +182,9 @@ describe('alert_edit', () => { actionTypeRegistry.has.mockReturnValue(true); wrapper = mountWithIntl( - {}} - initialAlert={alert} + initialRule={rule} onSave={() => { return new Promise(() => {}); }} @@ -199,32 +199,32 @@ describe('alert_edit', () => { }); } - it('renders alert edit flyout', async () => { + it('renders rule edit flyout', async () => { await setup(); - expect(wrapper.find('[data-test-subj="editAlertFlyoutTitle"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveEditedAlertButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="editRuleFlyoutTitle"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="saveEditedRuleButton"]').exists()).toBeTruthy(); }); it('displays a toast message on save for server errors', async () => { - const { isValidAlert } = jest.requireMock('./alert_errors'); - (isValidAlert as jest.Mock).mockImplementation(() => { + const { isValidRule } = jest.requireMock('./rule_errors'); + (isValidRule as jest.Mock).mockImplementation(() => { return true; }); await setup({ name: undefined }); await act(async () => { - wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="saveEditedRuleButton"]').first().simulate('click'); }); expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( 'Fail message' ); }); - it('should pass in the server alert type into `getAlertErrors`', async () => { - const { getAlertErrors } = jest.requireMock('./alert_errors'); + it('should pass in the server rule type into `getRuleErrors`', async () => { + const { getRuleErrors } = jest.requireMock('./rule_errors'); await setup(); - const lastCall = getAlertErrors.mock.calls[getAlertErrors.mock.calls.length - 1]; + const lastCall = getRuleErrors.mock.calls[getRuleErrors.mock.calls.length - 1]; expect(lastCall[2]).toBeDefined(); - expect(lastCall[2].id).toBe('my-alert-type'); + expect(lastCall[2].id).toBe('my-rule-type'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx similarity index 59% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index fc8f919f37901e..e2aaa21d5ba685 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -24,44 +24,38 @@ import { } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - Rule, - AlertFlyoutCloseReason, - AlertEditProps, - IErrorObject, - RuleType, -} from '../../../types'; -import { AlertForm } from './alert_form'; -import { getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_errors'; -import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; -import { updateAlert, loadAlertTypes } from '../../lib/alert_api'; +import { Rule, RuleFlyoutCloseReason, RuleEditProps, IErrorObject, RuleType } from '../../../types'; +import { RuleForm } from './rule_form'; +import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; +import { ruleReducer, ConcreteRuleReducer } from './rule_reducer'; +import { updateRule, loadRuleTypes } from '../../lib/rule_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; -import { ConfirmAlertClose } from './confirm_alert_close'; -import { hasAlertChanged } from './has_alert_changed'; -import { getAlertWithInvalidatedFields } from '../../lib/value_validators'; +import { ConfirmRuleClose } from './confirm_rule_close'; +import { hasRuleChanged } from './has_rule_changed'; +import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; -export const AlertEdit = ({ - initialAlert, +export const RuleEdit = ({ + initialRule, onClose, - reloadAlerts, + reloadRules, onSave, ruleTypeRegistry, actionTypeRegistry, metadata, ...props -}: AlertEditProps) => { - const onSaveHandler = onSave ?? reloadAlerts; - const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { - alert: cloneDeep(initialAlert), +}: RuleEditProps) => { + const onSaveHandler = onSave ?? reloadRules; + const [{ rule }, dispatch] = useReducer(ruleReducer as ConcreteRuleReducer, { + rule: cloneDeep(initialRule), }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState(false); - const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); - const [alertActionsErrors, setAlertActionsErrors] = useState([]); + const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState(false); + const [ruleActionsErrors, setRuleActionsErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); const [serverRuleType, setServerRuleType] = useState | undefined>( props.ruleType @@ -71,79 +65,74 @@ export const AlertEdit = ({ http, notifications: { toasts }, } = useKibana().services; - const setAlert = (value: Rule) => { - dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); + const setRule = (value: Rule) => { + dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); }; - const alertType = ruleTypeRegistry.get(alert.alertTypeId); + const ruleType = ruleTypeRegistry.get(rule.ruleTypeId); useEffect(() => { (async () => { setIsLoading(true); - const res = await getAlertActionErrors(alert as Rule, actionTypeRegistry); - setAlertActionsErrors([...res]); + const res = await getRuleActionErrors(rule as Rule, actionTypeRegistry); + setRuleActionsErrors([...res]); setIsLoading(false); })(); - }, [alert, actionTypeRegistry]); + }, [rule, actionTypeRegistry]); useEffect(() => { if (!props.ruleType && !serverRuleType) { (async () => { - const serverRuleTypes = await loadAlertTypes({ http }); + const serverRuleTypes = await loadRuleTypes({ http }); for (const _serverRuleType of serverRuleTypes) { - if (alertType.id === _serverRuleType.id) { + if (ruleType.id === _serverRuleType.id) { setServerRuleType(_serverRuleType); } } })(); } - }, [props.ruleType, alertType.id, serverRuleType, http]); + }, [props.ruleType, ruleType.id, serverRuleType, http]); - const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( - alert as Rule, - alertType, + const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = getRuleErrors( + rule as Rule, + ruleType, serverRuleType ); const checkForChangesAndCloseFlyout = () => { - if (hasAlertChanged(alert, initialAlert, true)) { - setIsConfirmAlertCloseModalOpen(true); + if (hasRuleChanged(rule, initialRule, true)) { + setIsConfirmRuleCloseModalOpen(true); } else { - onClose(AlertFlyoutCloseReason.CANCELED); + onClose(RuleFlyoutCloseReason.CANCELED); } }; - async function onSaveAlert(): Promise { + async function onSaveRule(): Promise { try { if ( !isLoading && - isValidAlert(alert, alertErrors, alertActionsErrors) && + isValidRule(rule, ruleErrors, ruleActionsErrors) && !hasActionsWithBrokenConnector ) { - const newAlert = await updateAlert({ http, alert, id: alert.id }); + const newRule = await updateRule({ http, rule, id: rule.id }); toasts.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText', { + i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText', { defaultMessage: "Updated '{ruleName}'", values: { - ruleName: newAlert.name, + ruleName: newRule.name, }, }) ); - return newAlert; + return newRule; } else { - setAlert( - getAlertWithInvalidatedFields( - alert as Rule, - alertParamsErrors, - alertBaseErrors, - alertActionsErrors - ) + setRule( + getRuleWithInvalidatedFields(rule, ruleParamsErrors, ruleBaseErrors, ruleActionsErrors) ); } } catch (errorRes) { toasts.addDanger( errorRes.body?.message ?? - i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText', { + i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText', { defaultMessage: 'Cannot update rule.', }) ); @@ -154,17 +143,17 @@ export const AlertEdit = ({ - +

@@ -177,26 +166,26 @@ export const AlertEdit = ({ )} - checkForChangesAndCloseFlyout()} > - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - )} + {i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} {isLoading ? ( @@ -229,16 +215,16 @@ export const AlertEdit = ({ { setIsSaving(true); - const savedAlert = await onSaveAlert(); + const savedRule = await onSaveRule(); setIsSaving(false); - if (savedAlert) { - onClose(AlertFlyoutCloseReason.SAVED); + if (savedRule) { + onClose(RuleFlyoutCloseReason.SAVED); if (onSaveHandler) { onSaveHandler(); } @@ -246,7 +232,7 @@ export const AlertEdit = ({ }} > @@ -255,14 +241,14 @@ export const AlertEdit = ({
- {isConfirmAlertCloseModalOpen && ( - { - setIsConfirmAlertCloseModalOpen(false); - onClose(AlertFlyoutCloseReason.CANCELED); + setIsConfirmRuleCloseModalOpen(false); + onClose(RuleFlyoutCloseReason.CANCELED); }} onCancel={() => { - setIsConfirmAlertCloseModalOpen(false); + setIsConfirmRuleCloseModalOpen(false); }} /> )} @@ -272,4 +258,4 @@ export const AlertEdit = ({ }; // eslint-disable-next-line import/no-default-export -export { AlertEdit as default }; +export { RuleEdit as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx similarity index 74% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx index 03e98b83615f27..d6232680b04ce2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx @@ -9,70 +9,70 @@ import uuid from 'uuid'; import React, { Fragment } from 'react'; import { validateBaseProperties, - getAlertErrors, - getAlertActionErrors, + getRuleErrors, + getRuleActionErrors, hasObjectErrors, - isValidAlert, -} from './alert_errors'; + isValidRule, +} from './rule_errors'; import { Rule, RuleType, RuleTypeModel } from '../../../types'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -describe('alert_errors', () => { +describe('rule_errors', () => { describe('validateBaseProperties()', () => { it('should validate the name', () => { - const alert = mockAlert(); - alert.name = ''; - const result = validateBaseProperties(alert); + const rule = mockRule(); + rule.name = ''; + const result = validateBaseProperties(rule); expect(result.errors).toStrictEqual({ name: ['Name is required.'], interval: [], - alertTypeId: [], + ruleTypeId: [], actionConnectors: [], }); }); it('should validate the interval', () => { - const alert = mockAlert(); - alert.schedule.interval = ''; - const result = validateBaseProperties(alert); + const rule = mockRule(); + rule.schedule.interval = ''; + const result = validateBaseProperties(rule); expect(result.errors).toStrictEqual({ name: [], interval: ['Check interval is required.'], - alertTypeId: [], + ruleTypeId: [], actionConnectors: [], }); }); it('should validate the minimumScheduleInterval', () => { - const alert = mockAlert(); - alert.schedule.interval = '2m'; + const rule = mockRule(); + rule.schedule.interval = '2m'; const result = validateBaseProperties( - alert, + rule, mockserverRuleType({ minimumScheduleInterval: '5m' }) ); expect(result.errors).toStrictEqual({ name: [], interval: ['Interval is below minimum (5m) for this rule type'], - alertTypeId: [], + ruleTypeId: [], actionConnectors: [], }); }); - it('should validate the alertTypeId', () => { - const alert = mockAlert(); - alert.alertTypeId = ''; - const result = validateBaseProperties(alert); + it('should validate the ruleTypeId', () => { + const rule = mockRule(); + rule.ruleTypeId = ''; + const result = validateBaseProperties(rule); expect(result.errors).toStrictEqual({ name: [], interval: [], - alertTypeId: ['Rule type is required.'], + ruleTypeId: ['Rule type is required.'], actionConnectors: [], }); }); it('should validate the connectors', () => { - const alert = mockAlert(); - alert.actions = [ + const rule = mockRule(); + rule.actions = [ { id: '1234', actionTypeId: 'myActionType', @@ -82,23 +82,23 @@ describe('alert_errors', () => { }, }, ]; - const result = validateBaseProperties(alert); + const result = validateBaseProperties(rule); expect(result.errors).toStrictEqual({ name: [], interval: [], - alertTypeId: [], + ruleTypeId: [], actionConnectors: ['Action for myActionType connector is required.'], }); }); }); - describe('getAlertErrors()', () => { + describe('getRuleErrors()', () => { it('should return all errors', () => { - const result = getAlertErrors( - mockAlert({ + const result = getRuleErrors( + mockRule({ name: '', }), - mockAlertTypeModel({ + mockRuleTypeModel({ validate: () => ({ errors: { field: ['This is wrong'], @@ -108,25 +108,25 @@ describe('alert_errors', () => { mockserverRuleType() ); expect(result).toStrictEqual({ - alertParamsErrors: { field: ['This is wrong'] }, - alertBaseErrors: { + ruleParamsErrors: { field: ['This is wrong'] }, + ruleBaseErrors: { name: ['Name is required.'], interval: [], - alertTypeId: [], + ruleTypeId: [], actionConnectors: [], }, - alertErrors: { + ruleErrors: { name: ['Name is required.'], field: ['This is wrong'], interval: [], - alertTypeId: [], + ruleTypeId: [], actionConnectors: [], }, }); }); }); - describe('getAlertActionErrors()', () => { + describe('getRuleActionErrors()', () => { it('should return an array of errors', async () => { const actionTypeRegistry = actionTypeRegistryMock.create(); actionTypeRegistry.get.mockImplementation((actionTypeId: string) => ({ @@ -137,8 +137,8 @@ describe('alert_errors', () => { }, })), })); - const result = await getAlertActionErrors( - mockAlert({ + const result = await getRuleActionErrors( + mockRule({ actions: [ { id: '1234', @@ -191,15 +191,15 @@ describe('alert_errors', () => { }); }); - describe('isValidAlert()', () => { - it('should return true for a valid alert', () => { - const result = isValidAlert(mockAlert(), {}, []); + describe('isValidRule()', () => { + it('should return true for a valid rule', () => { + const result = isValidRule(mockRule(), {}, []); expect(result).toBe(true); }); - it('should return false for an invalid alert', () => { + it('should return false for an invalid rule', () => { expect( - isValidAlert( - mockAlert(), + isValidRule( + mockRule(), { name: ['This is wrong'], }, @@ -207,7 +207,7 @@ describe('alert_errors', () => { ) ).toBe(false); expect( - isValidAlert(mockAlert(), {}, [ + isValidRule(mockRule(), {}, [ { name: ['This is wrong'], }, @@ -228,8 +228,8 @@ function mockserverRuleType( id: 'recovery', name: 'doRecovery', }, - id: 'myAppAlertType', - name: 'myAppAlertType', + id: 'myAppRuleType', + name: 'myAppRuleType', producer: 'myApp', authorizedConsumers: {}, enabledInLicense: true, @@ -242,10 +242,10 @@ function mockserverRuleType( }; } -function mockAlertTypeModel(overloads: Partial = {}): RuleTypeModel { +function mockRuleTypeModel(overloads: Partial = {}): RuleTypeModel { return { - id: 'alertTypeModel', - description: 'some alert', + id: 'ruleTypeModel', + description: 'some rule', iconClass: 'something', documentationUrl: null, validate: () => ({ errors: {} }), @@ -255,13 +255,13 @@ function mockAlertTypeModel(overloads: Partial = {}): RuleTypeMod }; } -function mockAlert(overloads: Partial = {}): Rule { +function mockRule(overloads: Partial = {}): Rule { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], - alertTypeId: '.noop', + ruleTypeId: '.noop', consumer: 'consumer', schedule: { interval: '1m' }, actions: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts similarity index 61% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts index d3e0f93d1d0b62..86a6b025941f5c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts @@ -11,44 +11,44 @@ import { RuleTypeModel, Rule, IErrorObject, - AlertAction, + RuleAction, RuleType, ValidationResult, ActionTypeRegistryContract, } from '../../../types'; -import { InitialAlert } from './alert_reducer'; +import { InitialRule } from './rule_reducer'; export function validateBaseProperties( - alertObject: InitialAlert, + ruleObject: InitialRule, serverRuleType?: RuleType ): ValidationResult { const validationResult = { errors: {} }; const errors = { name: new Array(), interval: new Array(), - alertTypeId: new Array(), + ruleTypeId: new Array(), actionConnectors: new Array(), }; validationResult.errors = errors; - if (!alertObject.name) { + if (!ruleObject.name) { errors.name.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredNameText', { + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText', { defaultMessage: 'Name is required.', }) ); } - if (alertObject.schedule.interval.length < 2) { + if (ruleObject.schedule.interval.length < 2) { errors.interval.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText', { defaultMessage: 'Check interval is required.', }) ); } else if (serverRuleType?.minimumScheduleInterval) { - const duration = parseDuration(alertObject.schedule.interval); + const duration = parseDuration(ruleObject.schedule.interval); const minimumDuration = parseDuration(serverRuleType.minimumScheduleInterval); if (duration < minimumDuration) { errors.interval.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.belowMinimumText', { + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText', { defaultMessage: 'Interval is below minimum ({minimum}) for this rule type', values: { minimum: serverRuleType.minimumScheduleInterval, @@ -58,19 +58,19 @@ export function validateBaseProperties( } } - if (!alertObject.alertTypeId) { - errors.alertTypeId.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText', { + if (!ruleObject.ruleTypeId) { + errors.ruleTypeId.push( + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText', { defaultMessage: 'Rule type is required.', }) ); } - const emptyConnectorActions = alertObject.actions.find( + const emptyConnectorActions = ruleObject.actions.find( (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 ); if (emptyConnectorActions !== undefined) { errors.actionConnectors.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector', { + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector', { defaultMessage: 'Action for {actionTypeId} connector is required.', values: { actionTypeId: emptyConnectorActions.actionTypeId }, }) @@ -79,36 +79,36 @@ export function validateBaseProperties( return validationResult; } -export function getAlertErrors( - alert: Rule, - alertTypeModel: RuleTypeModel | null, +export function getRuleErrors( + rule: Rule, + ruleTypeModel: RuleTypeModel | null, serverRuleType?: RuleType ) { - const alertParamsErrors: IErrorObject = alertTypeModel - ? alertTypeModel.validate(alert.params).errors + const ruleParamsErrors: IErrorObject = ruleTypeModel + ? ruleTypeModel.validate(rule.params).errors : []; - const alertBaseErrors = validateBaseProperties(alert, serverRuleType).errors as IErrorObject; - const alertErrors = { - ...alertParamsErrors, - ...alertBaseErrors, + const ruleBaseErrors = validateBaseProperties(rule, serverRuleType).errors as IErrorObject; + const ruleErrors = { + ...ruleParamsErrors, + ...ruleBaseErrors, } as IErrorObject; return { - alertParamsErrors, - alertBaseErrors, - alertErrors, + ruleParamsErrors, + ruleBaseErrors, + ruleErrors, }; } -export async function getAlertActionErrors( - alert: Rule, +export async function getRuleActionErrors( + rule: Rule, actionTypeRegistry: ActionTypeRegistryContract ): Promise { return await Promise.all( - alert.actions.map( - async (alertAction: AlertAction) => + rule.actions.map( + async (ruleAction: RuleAction) => ( - await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) + await actionTypeRegistry.get(ruleAction.actionTypeId)?.validateParams(ruleAction.params) ).errors ) ); @@ -120,11 +120,11 @@ export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => return errorList.length >= 1; }); -export function isValidAlert( - alertObject: InitialAlert | Rule, +export function isValidRule( + ruleObject: InitialRule | Rule, validationResult: IErrorObject, actionsErrors: IErrorObject[] -): alertObject is Rule { +): ruleObject is Rule { return ( !hasObjectErrors(validationResult) && actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss similarity index 56% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss index 5d6ac684002fb2..d80a80faac0a52 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss @@ -1,4 +1,4 @@ -.triggersActionsUI__alertTypeNodeHeading { +.triggersActionsUI__ruleTypeNodeHeading { margin-left: $euiSizeS; margin-right: $euiSizeS; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx similarity index 67% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index 49803c0cc419df..bda5d27ed54adf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -18,23 +18,23 @@ import { ConnectorValidationResult, GenericValidationResult, } from '../../../types'; -import { AlertForm } from './alert_form'; +import { RuleForm } from './rule_form'; import { coreMock } from 'src/core/public/mocks'; import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '../../../../../alerting/common'; import { useKibana } from '../../../common/lib/kibana'; const actionTypeRegistry = actionTypeRegistryMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); -jest.mock('../../lib/alert_api', () => ({ - loadAlertTypes: jest.fn(), +jest.mock('../../lib/rule_api', () => ({ + loadRuleTypes: jest.fn(), })); jest.mock('../../../common/lib/kibana'); -describe('alert_form', () => { +describe('rule_form', () => { const ruleType = { - id: 'my-alert-type', + id: 'my-rule-type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: (): ValidationResult => { return { errors: {} }; @@ -65,7 +65,7 @@ describe('alert_form', () => { }); const ruleTypeNonEditable = { - id: 'non-edit-alert-type', + id: 'non-edit-rule-type', iconClass: 'test', description: 'test', documentationUrl: null, @@ -79,7 +79,7 @@ describe('alert_form', () => { const disabledByLicenseRuleType = { id: 'disabled-by-license', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: (): ValidationResult => { return { errors: {} }; @@ -90,15 +90,15 @@ describe('alert_form', () => { const useKibanaMock = useKibana as jest.Mocked; - describe('alert_form create alert', () => { + describe('rule_form create rule', () => { let wrapper: ReactWrapper; async function setup() { const mocks = coreMock.createSetup(); - const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); - const alertTypes: RuleType[] = [ + const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); + const ruleTypes: RuleType[] = [ { - id: 'my-alert-type', + id: 'my-rule-type', name: 'Test', actionGroups: [ { @@ -144,7 +144,7 @@ describe('alert_form', () => { enabledInLicense: false, }, ]; - loadAlertTypes.mockResolvedValue(alertTypes); + loadRuleTypes.mockResolvedValue(ruleTypes); const [ { application: { capabilities }, @@ -153,7 +153,7 @@ describe('alert_form', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.application.capabilities = { ...capabilities, - alerts: { + rules: { show: true, save: true, delete: true, @@ -168,7 +168,7 @@ describe('alert_form', () => { actionTypeRegistry.list.mockReturnValue([actionType]); actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); - const initialAlert = { + const initialRule = { name: 'test', params: {}, consumer: ALERTS_FEATURE_ID, @@ -183,10 +183,10 @@ describe('alert_form', () => { } as unknown as Rule; wrapper = mountWithIntl( - {}} - errors={{ name: [], interval: [], alertTypeId: [] }} + errors={{ name: [], interval: [], ruleTypeId: [] }} operation="create" actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} @@ -199,52 +199,52 @@ describe('alert_form', () => { }); } - it('renders alert name', async () => { + it('renders rule name', async () => { await setup(); - const alertNameField = wrapper.find('[data-test-subj="alertNameInput"]'); - expect(alertNameField.exists()).toBeTruthy(); - expect(alertNameField.first().prop('value')).toBe('test'); + const ruleNameField = wrapper.find('[data-test-subj="ruleNameInput"]'); + expect(ruleNameField.exists()).toBeTruthy(); + expect(ruleNameField.first().prop('value')).toBe('test'); }); - it('renders registered selected alert type', async () => { + it('renders registered selected rule type', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find('[data-test-subj="my-alert-type-SelectOption"]'); - expect(alertTypeSelectOptions.exists()).toBeTruthy(); + const ruleTypeSelectOptions = wrapper.find('[data-test-subj="my-rule-type-SelectOption"]'); + expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); - it('does not render registered alert type which non editable', async () => { + it('does not render registered rule type which non editable', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find( - '[data-test-subj="non-edit-alert-type-SelectOption"]' + const ruleTypeSelectOptions = wrapper.find( + '[data-test-subj="non-edit-rule-type-SelectOption"]' ); - expect(alertTypeSelectOptions.exists()).toBeFalsy(); + expect(ruleTypeSelectOptions.exists()).toBeFalsy(); }); it('renders registered action types', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find( + const ruleTypeSelectOptions = wrapper.find( '[data-test-subj=".server-log-ActionTypeSelectOption"]' ); - expect(alertTypeSelectOptions.exists()).toBeFalsy(); + expect(ruleTypeSelectOptions.exists()).toBeFalsy(); }); - it('renders alert type description', async () => { + it('renders rule type description', async () => { await setup(); - wrapper.find('button[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); - const alertDescription = wrapper.find('[data-test-subj="alertDescription"]'); - expect(alertDescription.exists()).toBeTruthy(); - expect(alertDescription.first().text()).toContain('Alert when testing'); + wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); + const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); + expect(ruleDescription.exists()).toBeTruthy(); + expect(ruleDescription.first().text()).toContain('Rule when testing'); }); - it('renders alert type documentation link', async () => { + it('renders rule type documentation link', async () => { await setup(); - wrapper.find('button[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); - const alertDocumentationLink = wrapper.find('[data-test-subj="alertDocumentationLink"]'); - expect(alertDocumentationLink.exists()).toBeTruthy(); - expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); + wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); + const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); + expect(ruleDocumentationLink.exists()).toBeTruthy(); + expect(ruleDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); }); - it('renders alert types disabled by license', async () => { + it('renders rule types disabled by license', async () => { await setup(); const actionOption = wrapper.find(`[data-test-subj="disabled-by-license-SelectOption"]`); expect(actionOption.exists()).toBeTruthy(); @@ -254,15 +254,15 @@ describe('alert_form', () => { }); }); - describe('alert_form create alert non alerting consumer and producer', () => { + describe('rule_form create rule non ruleing consumer and producer', () => { let wrapper: ReactWrapper; async function setup() { - const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); + const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); - loadAlertTypes.mockResolvedValue([ + loadRuleTypes.mockResolvedValue([ { - id: 'other-consumer-producer-alert-type', + id: 'other-consumer-producer-rule-type', name: 'Test', actionGroups: [ { @@ -280,7 +280,7 @@ describe('alert_form', () => { }, }, { - id: 'same-consumer-producer-alert-type', + id: 'same-consumer-producer-rule-type', name: 'Test', actionGroups: [ { @@ -307,7 +307,7 @@ describe('alert_form', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.application.capabilities = { ...capabilities, - alerts: { + rules: { show: true, save: true, delete: true, @@ -315,7 +315,7 @@ describe('alert_form', () => { }; ruleTypeRegistry.list.mockReturnValue([ { - id: 'same-consumer-producer-alert-type', + id: 'same-consumer-producer-rule-type', iconClass: 'test', description: 'test', documentationUrl: null, @@ -326,7 +326,7 @@ describe('alert_form', () => { requiresAppContext: true, }, { - id: 'other-consumer-producer-alert-type', + id: 'other-consumer-producer-rule-type', iconClass: 'test', description: 'test', documentationUrl: null, @@ -340,8 +340,8 @@ describe('alert_form', () => { ruleTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); - const initialAlert = { - name: 'non alerting consumer test', + const initialRule = { + name: 'non ruleing consumer test', params: {}, consumer: 'test', schedule: { @@ -355,10 +355,10 @@ describe('alert_form', () => { } as unknown as Rule; wrapper = mountWithIntl( - {}} - errors={{ name: [], interval: [], alertTypeId: [] }} + errors={{ name: [], interval: [], ruleTypeId: [] }} operation="create" actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} @@ -370,27 +370,27 @@ describe('alert_form', () => { wrapper.update(); }); - expect(loadAlertTypes).toHaveBeenCalled(); + expect(loadRuleTypes).toHaveBeenCalled(); } - it('renders alert type options which producer correspond to the alert consumer', async () => { + it('renders rule type options which producer correspond to the rule consumer', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find( - '[data-test-subj="same-consumer-producer-alert-type-SelectOption"]' + const ruleTypeSelectOptions = wrapper.find( + '[data-test-subj="same-consumer-producer-rule-type-SelectOption"]' ); - expect(alertTypeSelectOptions.exists()).toBeTruthy(); + expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); - it('does not render alert type options which producer does not correspond to the alert consumer', async () => { + it('does not render rule type options which producer does not correspond to the rule consumer', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find( - '[data-test-subj="other-consumer-producer-alert-type-SelectOption"]' + const ruleTypeSelectOptions = wrapper.find( + '[data-test-subj="other-consumer-producer-rule-type-SelectOption"]' ); - expect(alertTypeSelectOptions.exists()).toBeFalsy(); + expect(ruleTypeSelectOptions.exists()).toBeFalsy(); }); }); - describe('alert_form edit alert', () => { + describe('rule_form edit rule', () => { let wrapper: ReactWrapper; async function setup() { @@ -401,9 +401,9 @@ describe('alert_form', () => { actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); - const initialAlert = { + const initialRule = { name: 'test', - alertTypeId: ruleType.id, + ruleTypeId: ruleType.id, params: {}, consumer: ALERTS_FEATURE_ID, schedule: { @@ -417,10 +417,10 @@ describe('alert_form', () => { } as unknown as Rule; wrapper = mountWithIntl( - {}} - errors={{ name: [], interval: [], alertTypeId: [] }} + errors={{ name: [], interval: [], ruleTypeId: [] }} operation="create" actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} @@ -433,31 +433,31 @@ describe('alert_form', () => { }); } - it('renders alert name', async () => { + it('renders rule name', async () => { await setup(); - const alertNameField = wrapper.find('[data-test-subj="alertNameInput"]'); - expect(alertNameField.exists()).toBeTruthy(); - expect(alertNameField.first().prop('value')).toBe('test'); + const ruleNameField = wrapper.find('[data-test-subj="ruleNameInput"]'); + expect(ruleNameField.exists()).toBeTruthy(); + expect(ruleNameField.first().prop('value')).toBe('test'); }); - it('renders registered selected alert type', async () => { + it('renders registered selected rule type', async () => { await setup(); - const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); - expect(alertTypeSelectOptions.exists()).toBeTruthy(); + const ruleTypeSelectOptions = wrapper.find('[data-test-subj="selectedRuleTypeTitle"]'); + expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); - it('renders alert type description', async () => { + it('renders rule type description', async () => { await setup(); - const alertDescription = wrapper.find('[data-test-subj="alertDescription"]'); - expect(alertDescription.exists()).toBeTruthy(); - expect(alertDescription.first().text()).toContain('Alert when testing'); + const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); + expect(ruleDescription.exists()).toBeTruthy(); + expect(ruleDescription.first().text()).toContain('Rule when testing'); }); - it('renders alert type documentation link', async () => { + it('renders rule type documentation link', async () => { await setup(); - const alertDocumentationLink = wrapper.find('[data-test-subj="alertDocumentationLink"]'); - expect(alertDocumentationLink.exists()).toBeTruthy(); - expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); + const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); + expect(ruleDocumentationLink.exists()).toBeTruthy(); + expect(ruleDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx similarity index 63% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index fa226c4a74cdd1..c7a87aa1fec11a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -41,13 +41,13 @@ import { getDurationNumberInItsUnit, getDurationUnitValue, } from '../../../../../alerting/common/parse_duration'; -import { loadAlertTypes } from '../../lib/alert_api'; -import { AlertReducerAction, InitialAlert } from './alert_reducer'; +import { loadRuleTypes } from '../../lib/rule_api'; +import { RuleReducerAction, InitialRule } from './rule_reducer'; import { RuleTypeModel, Rule, IErrorObject, - AlertAction, + RuleAction, RuleTypeIndex, RuleType, RuleTypeRegistryContract, @@ -56,21 +56,21 @@ import { import { getTimeOptions } from '../../../common/lib/get_time_options'; import { ActionForm } from '../action_connector_form'; import { - AlertActionParam, + AlertActionParam as RuleActionParam, ALERTS_FEATURE_ID, RecoveredActionGroup, isActionGroupDisabledForActionTypeId, } from '../../../../../alerting/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; import { SolutionFilter } from './solution_filter'; -import './alert_form.scss'; +import './rule_form.scss'; import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage } from '../../constants'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; -import { IsEnabledResult, IsDisabledResult } from '../../lib/check_alert_type_enabled'; -import { AlertNotifyWhen } from './alert_notify_when'; -import { checkAlertTypeEnabled } from '../../lib/check_alert_type_enabled'; -import { alertTypeCompare, alertTypeGroupCompare } from '../../lib/alert_type_compare'; +import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_enabled'; +import { RuleNotifyWhen } from './rule_notify_when'; +import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; +import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { SectionLoading } from '../../components/section_loading'; import { DEFAULT_ALERT_INTERVAL } from '../../constants'; @@ -81,9 +81,9 @@ function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[ return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } -interface AlertFormProps> { - alert: InitialAlert; - dispatch: React.Dispatch; +interface RuleFormProps> { + rule: InitialRule; + dispatch: React.Dispatch; errors: IErrorObject; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; @@ -97,8 +97,8 @@ interface AlertFormProps> { const defaultScheduleInterval = getDurationNumberInItsUnit(DEFAULT_ALERT_INTERVAL); const defaultScheduleIntervalUnit = getDurationUnitValue(DEFAULT_ALERT_INTERVAL); -export const AlertForm = ({ - alert, +export const RuleForm = ({ + rule, canChangeTrigger = true, dispatch, errors, @@ -108,7 +108,7 @@ export const AlertForm = ({ ruleTypeRegistry, actionTypeRegistry, metadata, -}: AlertFormProps) => { +}: RuleFormProps) => { const { http, notifications: { toasts }, @@ -120,65 +120,65 @@ export const AlertForm = ({ } = useKibana().services; const canShowActions = hasShowActionsCapability(capabilities); - const [alertTypeModel, setAlertTypeModel] = useState(null); + const [ruleTypeModel, setRuleTypeModel] = useState(null); - const [alertInterval, setAlertInterval] = useState( - alert.schedule.interval - ? getDurationNumberInItsUnit(alert.schedule.interval) + const [ruleInterval, setRuleInterval] = useState( + rule.schedule.interval + ? getDurationNumberInItsUnit(rule.schedule.interval) : defaultScheduleInterval ); - const [alertIntervalUnit, setAlertIntervalUnit] = useState( - alert.schedule.interval - ? getDurationUnitValue(alert.schedule.interval) + const [ruleIntervalUnit, setRuleIntervalUnit] = useState( + rule.schedule.interval + ? getDurationUnitValue(rule.schedule.interval) : defaultScheduleIntervalUnit ); - const [alertThrottle, setAlertThrottle] = useState( - alert.throttle ? getDurationNumberInItsUnit(alert.throttle) : null + const [ruleThrottle, setRuleThrottle] = useState( + rule.throttle ? getDurationNumberInItsUnit(rule.throttle) : null ); - const [alertThrottleUnit, setAlertThrottleUnit] = useState( - alert.throttle ? getDurationUnitValue(alert.throttle) : 'h' + const [ruleThrottleUnit, setRuleThrottleUnit] = useState( + rule.throttle ? getDurationUnitValue(rule.throttle) : 'h' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); const [ruleTypeIndex, setRuleTypeIndex] = useState(null); - const [availableAlertTypes, setAvailableAlertTypes] = useState< - Array<{ alertTypeModel: RuleTypeModel; alertType: RuleType }> + const [availableRuleTypes, setAvailableRuleTypes] = useState< + Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }> >([]); - const [filteredAlertTypes, setFilteredAlertTypes] = useState< - Array<{ alertTypeModel: RuleTypeModel; alertType: RuleType }> + const [filteredRuleTypes, setFilteredRuleTypes] = useState< + Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }> >([]); const [searchText, setSearchText] = useState(); const [inputText, setInputText] = useState(); const [solutions, setSolutions] = useState | undefined>(undefined); const [solutionsFilter, setSolutionFilter] = useState([]); - let hasDisabledByLicenseAlertTypes: boolean = false; + let hasDisabledByLicenseRuleTypes: boolean = false; - // load alert types + // load rule types useEffect(() => { (async () => { try { - const alertTypesResult = await loadAlertTypes({ http }); + const ruleTypesResult = await loadRuleTypes({ http }); const index: RuleTypeIndex = new Map(); - for (const alertTypeItem of alertTypesResult) { - index.set(alertTypeItem.id, alertTypeItem); + for (const ruleTypeItem of ruleTypesResult) { + index.set(ruleTypeItem.id, ruleTypeItem); } - if (alert.alertTypeId && index.has(alert.alertTypeId)) { - setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); + if (rule.ruleTypeId && index.has(rule.ruleTypeId)) { + setDefaultActionGroupId(index.get(rule.ruleTypeId)!.defaultActionGroupId); } setRuleTypeIndex(index); - const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); - setAvailableAlertTypes(availableAlertTypesResult); + const availableRuleTypesResult = getAvailableRuleTypes(ruleTypesResult); + setAvailableRuleTypes(availableRuleTypesResult); - const solutionsResult = availableAlertTypesResult.reduce( - (result: Map, alertTypeItem) => { - if (!result.has(alertTypeItem.alertType.producer)) { + const solutionsResult = availableRuleTypesResult.reduce( + (result: Map, ruleTypeItem) => { + if (!result.has(ruleTypeItem.ruleType.producer)) { result.set( - alertTypeItem.alertType.producer, + ruleTypeItem.ruleType.producer, (kibanaFeatures - ? getProducerFeatureName(alertTypeItem.alertType.producer, kibanaFeatures) - : capitalize(alertTypeItem.alertType.producer)) ?? - capitalize(alertTypeItem.alertType.producer) + ? getProducerFeatureName(ruleTypeItem.ruleType.producer, kibanaFeatures) + : capitalize(ruleTypeItem.ruleType.producer)) ?? + capitalize(ruleTypeItem.ruleType.producer) ); } return result; @@ -191,7 +191,7 @@ export const AlertForm = ({ } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadRuleTypesMessage', + 'xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage', { defaultMessage: 'Unable to load rule types' } ), }); @@ -201,25 +201,25 @@ export const AlertForm = ({ }, []); useEffect(() => { - setAlertTypeModel(alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null); - if (alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId)) { - setDefaultActionGroupId(ruleTypeIndex.get(alert.alertTypeId)!.defaultActionGroupId); + setRuleTypeModel(rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null); + if (rule.ruleTypeId && ruleTypeIndex && ruleTypeIndex.has(rule.ruleTypeId)) { + setDefaultActionGroupId(ruleTypeIndex.get(rule.ruleTypeId)!.defaultActionGroupId); } - }, [alert, alert.alertTypeId, ruleTypeIndex, ruleTypeRegistry]); + }, [rule, rule.ruleTypeId, ruleTypeIndex, ruleTypeRegistry]); useEffect(() => { - if (alert.schedule.interval) { - const interval = getDurationNumberInItsUnit(alert.schedule.interval); - const intervalUnit = getDurationUnitValue(alert.schedule.interval); + if (rule.schedule.interval) { + const interval = getDurationNumberInItsUnit(rule.schedule.interval); + const intervalUnit = getDurationUnitValue(rule.schedule.interval); if (interval !== defaultScheduleInterval) { - setAlertInterval(interval); + setRuleInterval(interval); } if (intervalUnit !== defaultScheduleIntervalUnit) { - setAlertIntervalUnit(intervalUnit); + setRuleIntervalUnit(intervalUnit); } } - }, [alert.schedule.interval]); + }, [rule.schedule.interval]); const setRuleProperty = useCallback( (key: Key, value: Rule[Key] | null) => { @@ -229,7 +229,7 @@ export const AlertForm = ({ ); const setActions = useCallback( - (updatedActions: AlertAction[]) => setRuleProperty('actions', updatedActions), + (updatedActions: RuleAction[]) => setRuleProperty('actions', updatedActions), [setRuleProperty] ); @@ -241,70 +241,70 @@ export const AlertForm = ({ dispatch({ command: { type: 'setScheduleProperty' }, payload: { key, value } }); }; - const setActionProperty = ( + const setActionProperty = ( key: Key, - value: AlertAction[Key] | null, + value: RuleAction[Key] | null, index: number ) => { - dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } }); + dispatch({ command: { type: 'setRuleActionProperty' }, payload: { key, value, index } }); }; const setActionParamsProperty = useCallback( - (key: string, value: AlertActionParam, index: number) => { - dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } }); + (key: string, value: RuleActionParam, index: number) => { + dispatch({ command: { type: 'setRuleActionParams' }, payload: { key, value, index } }); }, [dispatch] ); useEffect(() => { const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null; - setFilteredAlertTypes( - availableAlertTypes - .filter((alertTypeItem) => + setFilteredRuleTypes( + availableRuleTypes + .filter((ruleTypeItem) => solutionsFilter.length > 0 - ? solutionsFilter.find((item) => alertTypeItem.alertType!.producer === item) - : alertTypeItem + ? solutionsFilter.find((item) => ruleTypeItem.ruleType!.producer === item) + : ruleTypeItem ) - .filter((alertTypeItem) => + .filter((ruleTypeItem) => searchValue - ? alertTypeItem.alertType.name.toString().toLocaleLowerCase().includes(searchValue) || - alertTypeItem.alertType!.producer.toLocaleLowerCase().includes(searchValue) || - alertTypeItem.alertTypeModel.description.toLocaleLowerCase().includes(searchValue) - : alertTypeItem + ? ruleTypeItem.ruleType.name.toString().toLocaleLowerCase().includes(searchValue) || + ruleTypeItem.ruleType!.producer.toLocaleLowerCase().includes(searchValue) || + ruleTypeItem.ruleTypeModel.description.toLocaleLowerCase().includes(searchValue) + : ruleTypeItem ) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ruleTypeRegistry, availableAlertTypes, searchText, JSON.stringify(solutionsFilter)]); + }, [ruleTypeRegistry, availableRuleTypes, searchText, JSON.stringify(solutionsFilter)]); - const getAvailableAlertTypes = (alertTypesResult: RuleType[]) => + const getAvailableRuleTypes = (ruleTypesResult: RuleType[]) => ruleTypeRegistry .list() .reduce( ( - arr: Array<{ alertType: RuleType; alertTypeModel: RuleTypeModel }>, + arr: Array<{ ruleType: RuleType; ruleTypeModel: RuleTypeModel }>, ruleTypeRegistryItem: RuleTypeModel ) => { - const alertType = alertTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); - if (alertType) { + const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); + if (ruleType) { arr.push({ - alertType, - alertTypeModel: ruleTypeRegistryItem, + ruleType, + ruleTypeModel: ruleTypeRegistryItem, }); } return arr; }, [] ) - .filter((item) => item.alertType && hasAllPrivilege(alert, item.alertType)) + .filter((item) => item.ruleType && hasAllPrivilege(rule, item.ruleType)) .filter((item) => - alert.consumer === ALERTS_FEATURE_ID - ? !item.alertTypeModel.requiresAppContext - : item.alertType!.producer === alert.consumer + rule.consumer === ALERTS_FEATURE_ID + ? !item.ruleTypeModel.requiresAppContext + : item.ruleType!.producer === rule.consumer ); - const selectedAlertType = alert?.alertTypeId ? ruleTypeIndex?.get(alert?.alertTypeId) : undefined; - const recoveryActionGroup = selectedAlertType?.recoveryActionGroup?.id; + const selectedRuleType = rule?.ruleTypeId ? ruleTypeIndex?.get(rule?.ruleTypeId) : undefined; + const recoveryActionGroup = selectedRuleType?.recoveryActionGroup?.id; const getDefaultActionParams = useCallback( - (actionTypeId: string, actionGroupId: string): Record | undefined => + (actionTypeId: string, actionGroupId: string): Record | undefined => getDefaultsForActionParams( actionTypeId, actionGroupId, @@ -313,12 +313,12 @@ export const AlertForm = ({ [recoveryActionGroup] ); - const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : []; + const tagsOptions = rule.tags ? rule.tags.map((label: string) => ({ label })) : []; const isActionGroupDisabledForActionType = useCallback( - (alertType: RuleType, actionGroupId: string, actionTypeId: string): boolean => { + (ruleType: RuleType, actionGroupId: string, actionTypeId: string): boolean => { return isActionGroupDisabledForActionTypeId( - actionGroupId === alertType?.recoveryActionGroup?.id + actionGroupId === ruleType?.recoveryActionGroup?.id ? RecoveredActionGroup.id : actionGroupId, actionTypeId @@ -327,11 +327,9 @@ export const AlertForm = ({ [] ); - const AlertParamsExpressionComponent = alertTypeModel - ? alertTypeModel.ruleParamsExpression - : null; + const RuleParamsExpressionComponent = ruleTypeModel ? ruleTypeModel.ruleParamsExpression : null; - const alertTypesByProducer = filteredAlertTypes.reduce( + const ruleTypesByProducer = filteredRuleTypes.reduce( ( result: Record< string, @@ -339,22 +337,22 @@ export const AlertForm = ({ id: string; name: string; checkEnabledResult: IsEnabledResult | IsDisabledResult; - alertTypeItem: RuleTypeModel; + ruleTypeItem: RuleTypeModel; }> >, - alertTypeValue + ruleTypeValue ) => { - const producer = alertTypeValue.alertType.producer; + const producer = ruleTypeValue.ruleType.producer; if (producer) { - const checkEnabledResult = checkAlertTypeEnabled(alertTypeValue.alertType); + const checkEnabledResult = checkRuleTypeEnabled(ruleTypeValue.ruleType); if (!checkEnabledResult.isEnabled) { - hasDisabledByLicenseAlertTypes = true; + hasDisabledByLicenseRuleTypes = true; } (result[producer] = result[producer] || []).push({ - name: alertTypeValue.alertType.name, - id: alertTypeValue.alertTypeModel.id, + name: ruleTypeValue.ruleType.name, + id: ruleTypeValue.ruleTypeModel.id, checkEnabledResult, - alertTypeItem: alertTypeValue.alertTypeModel, + ruleTypeItem: ruleTypeValue.ruleTypeModel, }); } return result; @@ -362,18 +360,18 @@ export const AlertForm = ({ {} ); - const alertTypeNodes = Object.entries(alertTypesByProducer) - .sort((a, b) => alertTypeGroupCompare(a, b, solutions)) + const ruleTypeNodes = Object.entries(ruleTypesByProducer) + .sort((a, b) => ruleTypeGroupCompare(a, b, solutions)) .map(([solution, items], groupIndex) => ( @@ -391,13 +389,13 @@ export const AlertForm = ({ {items - .sort((a, b) => alertTypeCompare(a, b)) + .sort((a, b) => ruleTypeCompare(a, b)) .map((item, index) => { - const alertTypeListItemHtml = ( + const ruleTypeListItemHtml = ( {item.name} -

{item.alertTypeItem.description}

+

{item.ruleTypeItem.description}

); @@ -409,22 +407,22 @@ export const AlertForm = ({ color="primary" label={ item.checkEnabledResult.isEnabled ? ( - alertTypeListItemHtml + ruleTypeListItemHtml ) : ( - {alertTypeListItemHtml} + {ruleTypeListItemHtml} ) } isDisabled={!item.checkEnabledResult.isEnabled} onClick={() => { - setRuleProperty('alertTypeId', item.id); + setRuleProperty('ruleTypeId', item.id); setActions([]); - setAlertTypeModel(item.alertTypeItem); + setRuleTypeModel(item.ruleTypeItem); setRuleProperty('params', {}); if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); @@ -438,15 +436,15 @@ export const AlertForm = ({
)); - const alertTypeDetails = ( + const ruleTypeDetails = ( <> - -
- {alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId) - ? ruleTypeIndex.get(alert.alertTypeId)!.name + +
+ {rule.ruleTypeId && ruleTypeIndex && ruleTypeIndex.has(rule.ruleTypeId) + ? ruleTypeIndex.get(rule.ruleTypeId)!.name : ''}
@@ -457,38 +455,38 @@ export const AlertForm = ({ iconType="cross" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel', + 'xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel', { defaultMessage: 'Delete', } )} onClick={() => { - setRuleProperty('alertTypeId', null); - setAlertTypeModel(null); + setRuleProperty('ruleTypeId', null); + setRuleTypeModel(null); setRuleProperty('params', {}); }} /> ) : null} - {alertTypeModel?.description && ( + {ruleTypeModel?.description && ( - - {alertTypeModel.description}  - {alertTypeModel?.documentationUrl && ( + + {ruleTypeModel.description}  + {ruleTypeModel?.documentationUrl && ( @@ -498,31 +496,31 @@ export const AlertForm = ({ )} - {AlertParamsExpressionComponent && + {RuleParamsExpressionComponent && defaultActionGroupId && - alert.alertTypeId && - selectedAlertType ? ( + rule.ruleTypeId && + selectedRuleType ? ( } > - {errors.actionConnectors.length >= 1 ? ( <> @@ -544,24 +542,24 @@ export const AlertForm = ({ ) : null} - isActionGroupDisabledForActionType(selectedAlertType, actionGroupId, actionTypeId) + isActionGroupDisabledForActionType(selectedRuleType, actionGroupId, actionTypeId) } - actionGroups={selectedAlertType.actionGroups.map((actionGroup) => - actionGroup.id === selectedAlertType.recoveryActionGroup.id + actionGroups={selectedRuleType.actionGroups.map((actionGroup) => + actionGroup.id === selectedRuleType.recoveryActionGroup.id ? { ...actionGroup, - omitMessageVariables: selectedAlertType.doesSetRecoveryContext + omitMessageVariables: selectedRuleType.doesSetRecoveryContext ? 'keepContext' : 'all', defaultActionMessage: recoveredActionGroupMessage, } - : { ...actionGroup, defaultActionMessage: alertTypeModel?.defaultActionMessage } + : { ...actionGroup, defaultActionMessage: ruleTypeModel?.defaultActionMessage } )} getDefaultActionParams={getDefaultActionParams} setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)} @@ -577,16 +575,16 @@ export const AlertForm = ({ ); - const labelForAlertChecked = ( + const labelForRuleChecked = ( <> {' '} } - isInvalid={errors.name.length > 0 && alert.name !== undefined} + isInvalid={errors.name.length > 0 && rule.name !== undefined} error={errors.name} > 0 && alert.name !== undefined} + isInvalid={errors.name.length > 0 && rule.name !== undefined} name="name" - data-test-subj="alertNameInput" - value={alert.name || ''} + data-test-subj="ruleNameInput" + value={rule.name || ''} onChange={(e) => { setRuleProperty('name', e.target.value); }} onBlur={() => { - if (!alert.name) { + if (!rule.name) { setRuleProperty('name', ''); } }} @@ -631,7 +629,7 @@ export const AlertForm = ({ @@ -654,7 +652,7 @@ export const AlertForm = ({ ); }} onBlur={() => { - if (!alert.tags) { + if (!rule.tags) { setRuleProperty('tags', []); } }} @@ -668,7 +666,7 @@ export const AlertForm = ({ 0} error={errors.interval} > @@ -678,25 +676,25 @@ export const AlertForm = ({ fullWidth min={1} isInvalid={errors.interval.length > 0} - value={alertInterval || ''} + value={ruleInterval || ''} name="interval" data-test-subj="intervalInput" onChange={(e) => { const interval = e.target.value !== '' ? parseInt(e.target.value, 10) : undefined; - setAlertInterval(interval); - setScheduleProperty('interval', `${e.target.value}${alertIntervalUnit}`); + setRuleInterval(interval); + setScheduleProperty('interval', `${e.target.value}${ruleIntervalUnit}`); }} /> { - setAlertIntervalUnit(e.target.value); - setScheduleProperty('interval', `${alertInterval}${e.target.value}`); + setRuleIntervalUnit(e.target.value); + setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); }} data-test-subj="intervalInputUnit" /> @@ -705,10 +703,10 @@ export const AlertForm = ({ - { setRuleProperty('notifyWhen', notifyWhen); @@ -717,8 +715,8 @@ export const AlertForm = ({ )} onThrottleChange={useCallback( (throttle: number | null, throttleUnit: string) => { - setAlertThrottle(throttle); - setAlertThrottleUnit(throttleUnit); + setRuleThrottle(throttle); + setRuleThrottleUnit(throttleUnit); setRuleProperty('throttle', throttle ? `${throttle}${throttleUnit}` : null); }, [setRuleProperty] @@ -727,15 +725,15 @@ export const AlertForm = ({ - {alertTypeModel ? ( - <>{alertTypeDetails} - ) : availableAlertTypes.length ? ( + {ruleTypeModel ? ( + <>{ruleTypeDetails} + ) : availableRuleTypes.length ? ( <>
@@ -766,7 +764,7 @@ export const AlertForm = ({ { setInputText(e.target.value); if (e.target.value === '') { @@ -779,7 +777,7 @@ export const AlertForm = ({ } }} placeholder={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle', + 'xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle', { defaultMessage: 'Search' } )} /> @@ -796,21 +794,21 @@ export const AlertForm = ({
- {errors.alertTypeId.length >= 1 && alert.alertTypeId !== undefined ? ( + {errors.ruleTypeId.length >= 1 && rule.ruleTypeId !== undefined ? ( <> - + ) : null} - {alertTypeNodes} + {ruleTypeNodes} ) : ruleTypeIndex ? ( - + ) : ( @@ -819,15 +817,15 @@ export const AlertForm = ({ ); }; -const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => ( +const NoAuthorizedRuleTypes = ({ operation }: { operation: string }) => ( @@ -837,7 +835,7 @@ const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => (

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx index c15f249fd76888..4098614c1a9060 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx @@ -11,9 +11,9 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { Rule } from '../../../types'; import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; -import { AlertNotifyWhen } from './alert_notify_when'; +import { RuleNotifyWhen } from './rule_notify_when'; -describe('alert_notify_when', () => { +describe('rule_notify_when', () => { beforeEach(() => { jest.resetAllMocks(); }); @@ -21,11 +21,11 @@ describe('alert_notify_when', () => { const onNotifyWhenChange = jest.fn(); const onThrottleChange = jest.fn(); - describe('action_frequency_form new alert', () => { + describe('action_frequency_form new rule', () => { let wrapper: ReactWrapper; async function setup(overrides = {}) { - const initialAlert = { + const initialRule = { name: 'test', params: {}, consumer: ALERTS_FEATURE_ID, @@ -42,8 +42,8 @@ describe('alert_notify_when', () => { } as unknown as Rule; wrapper = mountWithIntl( - > = [ +const NOTIFY_WHEN_OPTIONS: Array> = [ { value: 'onActionGroupChange', inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display', + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display', { defaultMessage: 'Only on status change', } @@ -43,14 +43,14 @@ const NOTIFY_WHEN_OPTIONS: Array> = [

@@ -60,9 +60,9 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ { value: 'onActiveAlert', inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.display', + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display', { - defaultMessage: 'Every time alert is active', + defaultMessage: 'Every time rule is active', } ), 'data-test-subj': 'onActiveAlert', @@ -70,15 +70,15 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ <>

@@ -88,7 +88,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ { value: 'onThrottleInterval', inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.display', + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display', { defaultMessage: 'On a custom action interval', } @@ -99,14 +99,14 @@ const NOTIFY_WHEN_OPTIONS: Array> = [

@@ -115,56 +115,56 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ }, ]; -interface AlertNotifyWhenProps { - alert: InitialAlert; +interface RuleNotifyWhenProps { + rule: InitialRule; throttle: number | null; throttleUnit: string; - onNotifyWhenChange: (notifyWhen: AlertNotifyWhenType) => void; + onNotifyWhenChange: (notifyWhen: RuleNotifyWhenType) => void; onThrottleChange: (throttle: number | null, throttleUnit: string) => void; } -export const AlertNotifyWhen = ({ - alert, +export const RuleNotifyWhen = ({ + rule, throttle, throttleUnit, onNotifyWhenChange, onThrottleChange, -}: AlertNotifyWhenProps) => { - const [alertThrottle, setAlertThrottle] = useState(throttle || 1); +}: RuleNotifyWhenProps) => { + const [ruleThrottle, setRuleThrottle] = useState(throttle || 1); const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState(false); const [notifyWhenValue, setNotifyWhenValue] = - useState(DEFAULT_NOTIFY_WHEN_VALUE); + useState(DEFAULT_NOTIFY_WHEN_VALUE); useEffect(() => { - if (alert.notifyWhen) { - setNotifyWhenValue(alert.notifyWhen); + if (rule.notifyWhen) { + setNotifyWhenValue(rule.notifyWhen); } else { // If 'notifyWhen' is not set, derive value from existence of throttle value - setNotifyWhenValue(alert.throttle ? 'onThrottleInterval' : 'onActiveAlert'); + setNotifyWhenValue(rule.throttle ? 'onThrottleInterval' : 'onActiveAlert'); } - }, [alert]); + }, [rule]); useEffect(() => { setShowCustomThrottleOpts(notifyWhenValue === 'onThrottleInterval'); }, [notifyWhenValue]); - const onNotifyWhenValueChange = useCallback((newValue: AlertNotifyWhenType) => { - onThrottleChange(newValue === 'onThrottleInterval' ? alertThrottle : null, throttleUnit); + const onNotifyWhenValueChange = useCallback((newValue: RuleNotifyWhenType) => { + onThrottleChange(newValue === 'onThrottleInterval' ? ruleThrottle : null, throttleUnit); onNotifyWhenChange(newValue); setNotifyWhenValue(newValue); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const labelForAlertRenotify = ( + const labelForRuleRenotify = ( <> {' '} @@ -173,7 +173,7 @@ export const AlertNotifyWhen = ({ return ( <> - + parseInt(value, 10)), filter((value) => !isNaN(value)), map((value) => { - setAlertThrottle(value); + setRuleThrottle(value); onThrottleChange(value, throttleUnit); }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts new file mode 100644 index 00000000000000..259cb5ed3ecea5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ruleReducer } from './rule_reducer'; +import { Rule } from '../../../types'; + +describe('rule reducer', () => { + let initialRule: Rule; + beforeAll(() => { + initialRule = { + params: {}, + consumer: 'rules', + ruleTypeId: null, + schedule: { + interval: '1m', + }, + actions: [], + tags: [], + notifyWhen: 'onActionGroupChange', + } as unknown as Rule; + }); + + // setRule + test('if modified rule was reset to initial', () => { + const rule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setProperty' }, + payload: { + key: 'name', + value: 'new name', + }, + } + ); + expect(rule.rule.name).toBe('new name'); + + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRule' }, + payload: { + key: 'rule', + value: initialRule, + }, + } + ); + expect(updatedRule.rule.name).toBeUndefined(); + }); + + test('if property name was changed', () => { + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setProperty' }, + payload: { + key: 'name', + value: 'new name', + }, + } + ); + expect(updatedRule.rule.name).toBe('new name'); + }); + + test('if initial schedule property was updated', () => { + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setScheduleProperty' }, + payload: { + key: 'interval', + value: '10s', + }, + } + ); + expect(updatedRule.rule.schedule.interval).toBe('10s'); + }); + + test('if rule params property was added and updated', () => { + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRuleParams' }, + payload: { + key: 'testParam', + value: 'new test params property', + }, + } + ); + expect(updatedRule.rule.params.testParam).toBe('new test params property'); + + const updatedRuleParamsProperty = ruleReducer( + { rule: updatedRule.rule }, + { + command: { type: 'setRuleParams' }, + payload: { + key: 'testParam', + value: 'test params property updated', + }, + } + ); + expect(updatedRuleParamsProperty.rule.params.testParam).toBe('test params property updated'); + }); + + test('if rule action params property was added and updated', () => { + initialRule.actions.push({ + id: '', + actionTypeId: 'testId', + group: 'Rule', + params: {}, + }); + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRuleActionParams' }, + payload: { + key: 'testActionParam', + value: 'new test action params property', + index: 0, + }, + } + ); + expect(updatedRule.rule.actions[0].params.testActionParam).toBe( + 'new test action params property' + ); + + const updatedRuleActionParamsProperty = ruleReducer( + { rule: updatedRule.rule }, + { + command: { type: 'setRuleActionParams' }, + payload: { + key: 'testActionParam', + value: 'test action params property updated', + index: 0, + }, + } + ); + expect(updatedRuleActionParamsProperty.rule.actions[0].params.testActionParam).toBe( + 'test action params property updated' + ); + }); + + test('if the existing rule action params property was set to undefined (when other connector was selected)', () => { + initialRule.actions.push({ + id: '', + actionTypeId: 'testId', + group: 'Rule', + params: { + testActionParam: 'some value', + }, + }); + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRuleActionParams' }, + payload: { + key: 'testActionParam', + value: undefined, + index: 0, + }, + } + ); + expect(updatedRule.rule.actions[0].params.testActionParam).toBe(undefined); + }); + + test('if rule action property was updated', () => { + initialRule.actions.push({ + id: '', + actionTypeId: 'testId', + group: 'Rule', + params: {}, + }); + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRuleActionProperty' }, + payload: { + key: 'group', + value: 'Warning', + index: 0, + }, + } + ); + expect(updatedRule.rule.actions[0].group).toBe('Warning'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts new file mode 100644 index 00000000000000..c9186ca1d01c85 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectAttribute } from 'kibana/public'; +import { isEqual } from 'lodash'; +import { Reducer } from 'react'; +import { + AlertActionParam as RuleActionParam, + IntervalSchedule, +} from '../../../../../alerting/common'; +import { Rule, RuleAction } from '../../../types'; + +export type InitialRule = Partial & + Pick; + +interface CommandType< + T extends + | 'setRule' + | 'setProperty' + | 'setScheduleProperty' + | 'setRuleParams' + | 'setRuleActionParams' + | 'setRuleActionProperty' +> { + type: T; +} + +export interface RuleState { + rule: InitialRule; +} + +interface Payload { + key: Keys; + value: Value; + index?: number; +} + +interface RulePayload { + key: Key; + value: Rule[Key] | null; + index?: number; +} + +interface RuleActionPayload { + key: Key; + value: RuleAction[Key] | null; + index?: number; +} + +interface RuleSchedulePayload { + key: Key; + value: IntervalSchedule[Key]; + index?: number; +} + +export type RuleReducerAction = + | { + command: CommandType<'setRule'>; + payload: Payload<'rule', InitialRule>; + } + | { + command: CommandType<'setProperty'>; + payload: RulePayload; + } + | { + command: CommandType<'setScheduleProperty'>; + payload: RuleSchedulePayload; + } + | { + command: CommandType<'setRuleParams'>; + payload: Payload; + } + | { + command: CommandType<'setRuleActionParams'>; + payload: Payload; + } + | { + command: CommandType<'setRuleActionProperty'>; + payload: RuleActionPayload; + }; + +export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>; +export type ConcreteRuleReducer = Reducer<{ rule: Rule }, RuleReducerAction>; + +export const ruleReducer = ( + state: { rule: RulePhase }, + action: RuleReducerAction +) => { + const { rule } = state; + + switch (action.command.type) { + case 'setRule': { + const { key, value } = action.payload as Payload<'rule', RulePhase>; + if (key === 'rule') { + return { + ...state, + rule: value, + }; + } else { + return state; + } + } + case 'setProperty': { + const { key, value } = action.payload as RulePayload; + if (isEqual(rule[key], value)) { + return state; + } else { + return { + ...state, + rule: { + ...rule, + [key]: value, + }, + }; + } + } + case 'setScheduleProperty': { + const { key, value } = action.payload as RuleSchedulePayload; + if (rule.schedule && isEqual(rule.schedule[key], value)) { + return state; + } else { + return { + ...state, + rule: { + ...rule, + schedule: { + ...rule.schedule, + [key]: value, + }, + }, + }; + } + } + case 'setRuleParams': { + const { key, value } = action.payload as Payload>; + if (isEqual(rule.params[key], value)) { + return state; + } else { + return { + ...state, + rule: { + ...rule, + params: { + ...rule.params, + [key]: value, + }, + }, + }; + } + } + case 'setRuleActionParams': { + const { key, value, index } = action.payload as Payload< + keyof RuleAction, + SavedObjectAttribute + >; + if ( + index === undefined || + rule.actions[index] == null || + (!!rule.actions[index][key] && isEqual(rule.actions[index][key], value)) + ) { + return state; + } else { + const oldAction = rule.actions.splice(index, 1)[0]; + const updatedAction = { + ...oldAction, + params: { + ...oldAction.params, + [key]: value, + }, + }; + rule.actions.splice(index, 0, updatedAction); + return { + ...state, + rule: { + ...rule, + actions: [...rule.actions], + }, + }; + } + } + case 'setRuleActionProperty': { + const { key, value, index } = action.payload as RuleActionPayload; + if (index === undefined || isEqual(rule.actions[index][key], value)) { + return state; + } else { + const oldAction = rule.actions.splice(index, 1)[0]; + const updatedAction = { + ...oldAction, + [key]: value, + }; + rule.actions.splice(index, 0, updatedAction); + return { + ...state, + rule: { + ...rule, + actions: [...rule.actions], + }, + }; + } + } + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx index f46ec441227bf6..c3a7510ee0a0c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx @@ -43,7 +43,7 @@ export const SolutionFilter: React.FunctionComponent = ({ data-test-subj="solutionsFilterButton" > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx index 531b06364fee00..a136413d53e42c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx @@ -44,7 +44,7 @@ export const ActionTypeFilter: React.FunctionComponent = data-test-subj="actionTypeFilterButton" > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss similarity index 75% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss index 9ebd43e107e279..ffe000073aa753 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss @@ -4,6 +4,6 @@ } } -button[data-test-subj='deleteAlert'] { +button[data-test-subj='deleteRule'] { color: $euiColorDanger; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index a036feea0fbcb5..8ce6736aee8ada 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -10,17 +10,17 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { CollapsedItemActions } from './collapsed_item_actions'; import { act } from 'react-dom/test-utils'; import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; -import { AlertTableItem, RuleTypeModel } from '../../../../types'; +import { RuleTableItem, RuleTypeModel } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); -const onAlertChanged = jest.fn(); -const onEditAlert = jest.fn(); -const setAlertsToDelete = jest.fn(); -const disableAlert = jest.fn(); -const enableAlert = jest.fn(); -const unmuteAlert = jest.fn(); -const muteAlert = jest.fn(); +const onRuleChanged = jest.fn(); +const onEditRule = jest.fn(); +const setRulesToDelete = jest.fn(); +const disableRule = jest.fn(); +const enableRule = jest.fn(); +const unmuteRule = jest.fn(); +const muteRule = jest.fn(); export const tick = (ms = 0) => new Promise((resolve) => { @@ -31,10 +31,10 @@ describe('CollapsedItemActions', () => { async function setup(editable: boolean = true) { const ruleTypeRegistry = ruleTypeRegistryMock.create(); ruleTypeRegistry.has.mockReturnValue(true); - const alertTypeR: RuleTypeModel = { - id: 'my-alert-type', + const ruleTypeR: RuleTypeModel = { + id: 'my-rule-type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: () => { return { errors: {} }; @@ -42,20 +42,20 @@ describe('CollapsedItemActions', () => { ruleParamsExpression: jest.fn(), requiresAppContext: !editable, }; - ruleTypeRegistry.get.mockReturnValue(alertTypeR); + ruleTypeRegistry.get.mockReturnValue(ruleTypeR); const useKibanaMock = useKibana as jest.Mocked; // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; } const getPropsWithRule = (overrides = {}, editable = false) => { - const rule: AlertTableItem = { + const rule: RuleTableItem = { id: '1', enabled: true, name: 'test rule', tags: ['tag1'], - alertTypeId: 'test_rule_type', - consumer: 'alerts', + ruleTypeId: 'test_rule_type', + consumer: 'rules', schedule: { interval: '5d' }, actions: [ { id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } }, @@ -76,7 +76,7 @@ describe('CollapsedItemActions', () => { }, actionsCount: 1, index: 0, - alertType: 'Test Alert Type', + ruleType: 'Test Rule Type', isEditable: true, enabledInLicense: true, ...overrides, @@ -84,13 +84,13 @@ describe('CollapsedItemActions', () => { return { item: rule, - onAlertChanged, - onEditAlert, - setAlertsToDelete, - disableAlert, - enableAlert, - unmuteAlert, - muteAlert, + onRuleChanged, + onEditRule, + setRulesToDelete, + disableRule, + enableRule, + unmuteRule, + muteRule, }; }; @@ -116,8 +116,8 @@ describe('CollapsedItemActions', () => { expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeFalsy(); wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); await act(async () => { @@ -128,8 +128,8 @@ describe('CollapsedItemActions', () => { expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeTruthy(); expect( wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled @@ -139,10 +139,10 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); }); test('handles case when rule is unmuted and enabled and mute is clicked', async () => { @@ -158,7 +158,7 @@ describe('CollapsedItemActions', () => { await tick(10); wrapper.update(); }); - expect(muteAlert).toHaveBeenCalled(); + expect(muteRule).toHaveBeenCalled(); }); test('handles case when rule is unmuted and enabled and disable is clicked', async () => { @@ -174,7 +174,7 @@ describe('CollapsedItemActions', () => { await tick(10); wrapper.update(); }); - expect(disableAlert).toHaveBeenCalled(); + expect(disableRule).toHaveBeenCalled(); }); test('handles case when rule is muted and enabled and unmute is clicked', async () => { @@ -192,7 +192,7 @@ describe('CollapsedItemActions', () => { await tick(10); wrapper.update(); }); - expect(unmuteAlert).toHaveBeenCalled(); + expect(unmuteRule).toHaveBeenCalled(); }); test('handles case when rule is unmuted and disabled and enable is clicked', async () => { @@ -210,7 +210,7 @@ describe('CollapsedItemActions', () => { await tick(10); wrapper.update(); }); - expect(enableAlert).toHaveBeenCalled(); + expect(enableRule).toHaveBeenCalled(); }); test('handles case when edit rule is clicked', async () => { @@ -221,12 +221,12 @@ describe('CollapsedItemActions', () => { await nextTick(); wrapper.update(); }); - wrapper.find('button[data-test-subj="editAlert"]').simulate('click'); + wrapper.find('button[data-test-subj="editRule"]').simulate('click'); await act(async () => { await nextTick(); wrapper.update(); }); - expect(onEditAlert).toHaveBeenCalled(); + expect(onEditRule).toHaveBeenCalled(); }); test('handles case when delete rule is clicked', async () => { @@ -237,12 +237,12 @@ describe('CollapsedItemActions', () => { await nextTick(); wrapper.update(); }); - wrapper.find('button[data-test-subj="deleteAlert"]').simulate('click'); + wrapper.find('button[data-test-subj="deleteRule"]').simulate('click'); await act(async () => { await nextTick(); wrapper.update(); }); - expect(setAlertsToDelete).toHaveBeenCalled(); + expect(setRulesToDelete).toHaveBeenCalled(); }); test('renders actions correctly when rule is disabled', async () => { @@ -260,10 +260,10 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Enable'); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); }); test('renders actions correctly when rule is not editable', async () => { @@ -297,10 +297,10 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeTruthy(); expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); }); test('renders actions correctly when rule is muted', async () => { @@ -318,10 +318,10 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Unmute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); }); test('renders actions correctly when rule type is not editable in this context', async () => { @@ -337,9 +337,9 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); - expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx similarity index 68% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index e2870e8097946b..4fcecc3410f179 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -11,29 +11,29 @@ import React, { useEffect, useState } from 'react'; import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; -import { AlertTableItem } from '../../../../types'; +import { RuleTableItem } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, - withBulkAlertOperations, -} from '../../common/components/with_bulk_alert_api_operations'; + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; import './collapsed_item_actions.scss'; export type ComponentOpts = { - item: AlertTableItem; - onAlertChanged: () => void; - setAlertsToDelete: React.Dispatch>; - onEditAlert: (item: AlertTableItem) => void; -} & Pick; + item: RuleTableItem; + onRuleChanged: () => void; + setRulesToDelete: React.Dispatch>; + onEditRule: (item: RuleTableItem) => void; +} & Pick; export const CollapsedItemActions: React.FunctionComponent = ({ item, - onAlertChanged, - disableAlert, - enableAlert, - unmuteAlert, - muteAlert, - setAlertsToDelete, - onEditAlert, + onRuleChanged, + disableRule, + enableRule, + unmuteRule, + muteRule, + setRulesToDelete, + onEditRule, }: ComponentOpts) => { const { ruleTypeRegistry } = useKibana().services; @@ -45,8 +45,8 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setIsMuted(item.muteAll); }, [item.enabled, item.muteAll]); - const isRuleTypeEditableInContext = ruleTypeRegistry.has(item.alertTypeId) - ? !ruleTypeRegistry.get(item.alertTypeId).requiresAppContext + const isRuleTypeEditableInContext = ruleTypeRegistry.has(item.ruleTypeId) + ? !ruleTypeRegistry.get(item.ruleTypeId).requiresAppContext : false; const button = ( @@ -56,7 +56,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ iconType="boxesHorizontal" onClick={() => setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle', { defaultMessage: 'Actions' } )} /> @@ -74,22 +74,22 @@ export const CollapsedItemActions: React.FunctionComponent = ({ const muteAll = isMuted; asyncScheduler.schedule(async () => { if (muteAll) { - await unmuteAlert({ ...item, muteAll }); + await unmuteRule({ ...item, muteAll }); } else { - await muteAlert({ ...item, muteAll }); + await muteRule({ ...item, muteAll }); } - onAlertChanged(); + onRuleChanged(); }, 10); setIsMuted(!isMuted); setIsPopoverOpen(!isPopoverOpen); }, name: isMuted ? i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.unmuteTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle', { defaultMessage: 'Unmute' } ) : i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle', { defaultMessage: 'Mute' } ), }, @@ -100,46 +100,46 @@ export const CollapsedItemActions: React.FunctionComponent = ({ const enabled = !isDisabled; asyncScheduler.schedule(async () => { if (enabled) { - await disableAlert({ ...item, enabled }); + await disableRule({ ...item, enabled }); } else { - await enableAlert({ ...item, enabled }); + await enableRule({ ...item, enabled }); } - onAlertChanged(); + onRuleChanged(); }, 10); setIsDisabled(!isDisabled); setIsPopoverOpen(!isPopoverOpen); }, name: isDisabled ? i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle', { defaultMessage: 'Enable' } ) : i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle', { defaultMessage: 'Disable' } ), }, { disabled: !item.isEditable || !isRuleTypeEditableInContext, - 'data-test-subj': 'editAlert', + 'data-test-subj': 'editRule', onClick: () => { setIsPopoverOpen(!isPopoverOpen); - onEditAlert(item); + onEditRule(item); }, name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.editTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle', { defaultMessage: 'Edit rule' } ), }, { disabled: !item.isEditable, - 'data-test-subj': 'deleteAlert', + 'data-test-subj': 'deleteRule', onClick: () => { setIsPopoverOpen(!isPopoverOpen); - setAlertsToDelete([item.id]); + setRulesToDelete([item.id]); }, name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteRuleTitle', + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.deleteRuleTitle', { defaultMessage: 'Delete rule' } ), }, @@ -166,4 +166,4 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ); }; -export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions); +export const CollapsedItemActionsWithApi = withBulkRuleOperations(CollapsedItemActions); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/manage_license_modal.tsx similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/manage_license_modal.tsx index 77cca21ad33cf8..eb7a616359bcdf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/manage_license_modal.tsx @@ -13,14 +13,14 @@ import { capitalize } from 'lodash'; interface Props { licenseType: string; - alertTypeId: string; + ruleTypeId: string; onConfirm: () => void; onCancel: () => void; } export const ManageLicenseModal: React.FC = ({ licenseType, - alertTypeId, + ruleTypeId, onConfirm, onCancel, }) => { @@ -51,8 +51,8 @@ export const ManageLicenseModal: React.FC = ({

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/percentile_selectable_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/percentile_selectable_popover.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/percentile_selectable_popover.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/percentile_selectable_popover.tsx index 807258522f3856..0d3f98cee5cd81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/percentile_selectable_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/percentile_selectable_popover.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiSelectable, EuiSelectableOption } from '@elastic/eui'; const iconButtonTitle = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileSelectButton', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleExecutionPercentileSelectButton', { defaultMessage: 'select percentile' } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_duration_format.tsx similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_duration_format.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.test.tsx similarity index 85% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.test.tsx index 0773d6c9c5ed0d..418b6931ca1a35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.test.tsx @@ -10,19 +10,19 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { RuleEnabledSwitch, ComponentOpts } from './rule_enabled_switch'; describe('RuleEnabledSwitch', () => { - const enableAlert = jest.fn(); + const enableRule = jest.fn(); const props: ComponentOpts = { - disableAlert: jest.fn(), - enableAlert, + disableRule: jest.fn(), + enableRule, item: { id: '1', - name: 'test alert', + name: 'test rule', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, createdBy: null, updatedBy: null, apiKeyOwner: null, @@ -35,7 +35,7 @@ describe('RuleEnabledSwitch', () => { }, consumer: 'test', actionsCount: 0, - alertType: 'test_alert_type', + ruleType: 'test_rule_type', createdAt: new Date('2020-08-20T19:23:38Z'), enabledInLicense: true, isEditable: false, @@ -43,7 +43,7 @@ describe('RuleEnabledSwitch', () => { index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), }, - onAlertChanged: jest.fn(), + onRuleChanged: jest.fn(), }; beforeEach(() => jest.resetAllMocks()); @@ -60,13 +60,13 @@ describe('RuleEnabledSwitch', () => { ...props, item: { id: '1', - name: 'test alert', + name: 'test rule', tags: ['tag1'], enabled: false, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, createdBy: null, updatedBy: null, apiKeyOwner: null, @@ -79,7 +79,7 @@ describe('RuleEnabledSwitch', () => { }, consumer: 'test', actionsCount: 0, - alertType: 'test_alert_type', + ruleType: 'test_rule_type', createdAt: new Date('2020-08-20T19:23:38Z'), enabledInLicense: true, isEditable: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.tsx similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.tsx index 036bfa68b8dbdf..5b612833ba937e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_enabled_switch.tsx @@ -8,20 +8,20 @@ import React, { useState, useEffect } from 'react'; import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; -import { Rule, AlertTableItem } from '../../../../types'; +import { Rule, RuleTableItem } from '../../../../types'; export interface ComponentOpts { - item: AlertTableItem; - onAlertChanged: () => void; - enableAlert: (alert: Rule) => Promise; - disableAlert: (alert: Rule) => Promise; + item: RuleTableItem; + onRuleChanged: () => void; + enableRule: (rule: Rule) => Promise; + disableRule: (rule: Rule) => Promise; } export const RuleEnabledSwitch: React.FunctionComponent = ({ item, - onAlertChanged, - disableAlert, - enableAlert, + onRuleChanged, + disableRule, + enableRule, }: ComponentOpts) => { const [isEnabled, setIsEnabled] = useState(!item.enabled); useEffect(() => { @@ -42,13 +42,13 @@ export const RuleEnabledSwitch: React.FunctionComponent = ({ setIsUpdating(true); const enabled = item.enabled; if (enabled) { - await disableAlert({ ...item, enabled }); + await disableRule({ ...item, enabled }); } else { - await enableAlert({ ...item, enabled }); + await enableRule({ ...item, enabled }); } setIsEnabled(!isEnabled); setIsUpdating(false); - onAlertChanged(); + onRuleChanged(); }} label="" /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx similarity index 82% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx index fb6580e576ce38..2f2b1914bef09c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx @@ -18,17 +18,17 @@ import { AlertExecutionStatuses, AlertExecutionStatusValues, } from '../../../../../../alerting/common'; -import { alertsStatusesTranslationsMapping } from '../translations'; +import { rulesStatusesTranslationsMapping } from '../translations'; -interface AlertStatusFilterProps { +interface RuleStatusFilterProps { selectedStatuses: string[]; - onChange?: (selectedAlertStatusesIds: string[]) => void; + onChange?: (selectedRuleStatusesIds: string[]) => void; } -export const AlertStatusFilter: React.FunctionComponent = ({ +export const RuleStatusFilter: React.FunctionComponent = ({ selectedStatuses, onChange, -}: AlertStatusFilterProps) => { +}: RuleStatusFilterProps) => { const [selectedValues, setSelectedValues] = useState(selectedStatuses); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -55,10 +55,10 @@ export const AlertStatusFilter: React.FunctionComponent numActiveFilters={selectedValues.length} numFilters={selectedValues.length} onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="alertStatusFilterButton" + data-test-subj="ruleStatusFilterButton" > @@ -80,9 +80,9 @@ export const AlertStatusFilter: React.FunctionComponent } }} checked={selectedValues.includes(item) ? 'on' : undefined} - data-test-subj={`alertStatus${item}FilerOption`} + data-test-subj={`ruleStatus${item}FilerOption`} > - {alertsStatusesTranslationsMapping[item]} + {rulesStatusesTranslationsMapping[item]} ); })} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.scss similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.scss index 138605421f202d..28db982b826bd2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.scss @@ -1,7 +1,7 @@ -.actAlertsList__tableRowDisabled { +.actRulesList__tableRowDisabled { background-color: $euiColorLightestShade; - .actAlertsList__tableCellDisabled { + .actRulesList__tableCellDisabled { color: $euiColorDarkShade; } } @@ -10,7 +10,7 @@ &:hover, &:focus-within, &[class*='-isActive'] { - .alertSidebarItem__action { + .ruleSidebarItem__action { opacity: 1; } } @@ -20,10 +20,10 @@ * 1. Only visually hide the action, so that it's still accessible to screen readers. * 2. When tabbed to, this element needs to be visible for keyboard accessibility. */ -.alertSidebarItem__action { +.ruleSidebarItem__action { opacity: 0; /* 1 */ - &.alertSidebarItem__mobile { + &.ruleSidebarItem__mobile { opacity: 1; } @@ -41,4 +41,4 @@ bottom: $euiSizeXS; margin-left: $euiSizeXS; position: relative; -} \ No newline at end of file +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx similarity index 70% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 28aa0b2097abad..adee7bd7a3c35d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -11,7 +11,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; -import { AlertsList, percentileFields } from './alerts_list'; +import { RulesList, percentileFields } from './rules_list'; import { RuleTypeModel, ValidationResult, Percentiles } from '../../../../types'; import { AlertExecutionStatusErrorReasons, @@ -27,38 +27,38 @@ jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), loadAllActions: jest.fn(), })); -jest.mock('../../../lib/alert_api', () => ({ - loadAlerts: jest.fn(), - loadAlertTypes: jest.fn(), +jest.mock('../../../lib/rule_api', () => ({ + loadRules: jest.fn(), + loadRuleTypes: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, })), })); jest.mock('../../../../common/lib/health_api', () => ({ - triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), + triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), })); jest.mock('react-router-dom', () => ({ useHistory: () => ({ push: jest.fn(), }), useLocation: () => ({ - pathname: '/triggersActions/alerts/', + pathname: '/triggersActions/rules/', }), })); jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), - hasSaveAlertsCapability: jest.fn(() => true), + hasSaveRulesCapability: jest.fn(() => true), hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); -const { loadAlerts, loadAlertTypes } = jest.requireMock('../../../lib/alert_api'); +const { loadRules, loadRuleTypes } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const ruleType = { - id: 'test_alert_type', + id: 'test_rule_type', description: 'test', iconClass: 'test', documentationUrl: null, @@ -68,9 +68,9 @@ const ruleType = { ruleParamsExpression: () => null, requiresAppContext: false, }; -const alertTypeFromApi = { - id: 'test_alert_type', - name: 'some alert type', +const ruleTypeFromApi = { + id: 'test_rule_type', + name: 'some rule type', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, actionVariables: { context: [], state: [] }, @@ -87,10 +87,10 @@ ruleTypeRegistry.list.mockReturnValue([ruleType]); actionTypeRegistry.list.mockReturnValue([]); const useKibanaMock = useKibana as jest.Mocked; -describe('alerts_list component empty', () => { +describe('rules_list component empty', () => { let wrapper: ReactWrapper; async function setup() { - loadAlerts.mockResolvedValue({ + loadRules.mockResolvedValue({ page: 1, perPage: 10000, total: 0, @@ -106,7 +106,7 @@ describe('alerts_list component empty', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([alertTypeFromApi]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); loadAllActions.mockResolvedValue([]); // eslint-disable-next-line react-hooks/rules-of-hooks @@ -115,7 +115,7 @@ describe('alerts_list component empty', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); await act(async () => { await nextTick(); @@ -125,20 +125,20 @@ describe('alerts_list component empty', () => { it('renders empty list', async () => { await setup(); - expect(wrapper.find('[data-test-subj="createFirstAlertEmptyPrompt"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="createFirstRuleEmptyPrompt"]').exists()).toBeTruthy(); }); - it('renders Create alert button', async () => { + it('renders Create rule button', async () => { await setup(); - expect( - wrapper.find('[data-test-subj="createFirstAlertButton"]').find('EuiButton') - ).toHaveLength(1); - expect(wrapper.find('AlertAdd').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="createFirstRuleButton"]').find('EuiButton')).toHaveLength( + 1 + ); + expect(wrapper.find('RuleAdd').exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="createFirstAlertButton"]').simulate('click'); + wrapper.find('button[data-test-subj="createFirstRuleButton"]').simulate('click'); await act(async () => { - // When the AlertAdd component is rendered, it waits for the healthcheck to resolve + // When the RuleAdd component is rendered, it waits for the healthcheck to resolve await new Promise((resolve) => { setTimeout(resolve, 1000); }); @@ -147,23 +147,23 @@ describe('alerts_list component empty', () => { wrapper.update(); }); - expect(wrapper.find('AlertAdd').exists()).toEqual(true); + expect(wrapper.find('RuleAdd').exists()).toEqual(true); }); }); -describe('alerts_list component with items', () => { +describe('rules_list component with items', () => { let wrapper: ReactWrapper; - const mockedAlertsData = [ + const mockedRulesData = [ { id: '1', - name: 'test alert', + name: 'test rule', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -204,13 +204,13 @@ describe('alerts_list component with items', () => { }, { id: '2', - name: 'test alert ok', + name: 'test rule ok', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -247,13 +247,13 @@ describe('alerts_list component with items', () => { }, { id: '3', - name: 'test alert pending', + name: 'test rule pending', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -278,13 +278,13 @@ describe('alerts_list component with items', () => { }, { id: '4', - name: 'test alert error', + name: 'test rule error', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], - params: { name: 'test alert type name' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -304,13 +304,13 @@ describe('alerts_list component with items', () => { }, { id: '5', - name: 'test alert license error', + name: 'test rule license error', tags: [], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], - params: { name: 'test alert type name' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -331,11 +331,11 @@ describe('alerts_list component with items', () => { ]; async function setup(editable: boolean = true) { - loadAlerts.mockResolvedValue({ + loadRules.mockResolvedValue({ page: 1, perPage: 10000, total: 4, - data: mockedAlertsData, + data: mockedRulesData, }); loadActionTypes.mockResolvedValue([ { @@ -347,13 +347,13 @@ describe('alerts_list component with items', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([alertTypeFromApi]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); loadAllActions.mockResolvedValue([]); const ruleTypeMock: RuleTypeModel = { - id: 'test_alert_type', + id: 'test_rule_type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: () => { return { errors: {} }; @@ -369,54 +369,54 @@ describe('alerts_list component with items', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); await act(async () => { await nextTick(); wrapper.update(); }); - expect(loadAlerts).toHaveBeenCalled(); + expect(loadRules).toHaveBeenCalled(); expect(loadActionTypes).toHaveBeenCalled(); } - it('renders table of alerts', async () => { + it('renders table of rules', async () => { // Use fake timers so we don't have to wait for the EuiToolTip timeout jest.useFakeTimers(); await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(mockedAlertsData.length); + expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length); // Enabled switch column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-enabled"]').length - ).toEqual(mockedAlertsData.length); + expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-enabled"]').length).toEqual( + mockedRulesData.length + ); // Name and rule type column - const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-name"]'); - expect(ruleNameColumns.length).toEqual(mockedAlertsData.length); - mockedAlertsData.forEach((rule, index) => { - expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${alertTypeFromApi.name}`); + const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]'); + expect(ruleNameColumns.length).toEqual(mockedRulesData.length); + mockedRulesData.forEach((rule, index) => { + expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${ruleTypeFromApi.name}`); }); // Tags column expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-tagsPopover"]').length - ).toEqual(mockedAlertsData.length); + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-tagsPopover"]').length + ).toEqual(mockedRulesData.length); // only show tags popover if tags exist on rule const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagsBadge"]'); expect(tagsBadges.length).toEqual( - mockedAlertsData.filter((data) => data.tags.length > 0).length + mockedRulesData.filter((data) => data.tags.length > 0).length ); // Last run column expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-lastExecutionDate"]').length - ).toEqual(mockedAlertsData.length); + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastExecutionDate"]').length + ).toEqual(mockedRulesData.length); // Last run tooltip wrapper - .find('[data-test-subj="alertsTableCell-lastExecutionDateTooltip"]') + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') .first() .simulate('mouseOver'); @@ -427,33 +427,29 @@ describe('alerts_list component with items', () => { expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last execution.'); wrapper - .find('[data-test-subj="alertsTableCell-lastExecutionDateTooltip"]') + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') .first() .simulate('mouseOut'); // Schedule interval column expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-interval"]').length - ).toEqual(mockedAlertsData.length); + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-interval"]').length + ).toEqual(mockedRulesData.length); // Duration column expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-duration"]').length - ).toEqual(mockedAlertsData.length); + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-duration"]').length + ).toEqual(mockedRulesData.length); // show warning if duration is long const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); expect(durationWarningIcon.length).toEqual( - mockedAlertsData.filter( - (data) => - data.executionStatus.lastDuration > parseDuration(alertTypeFromApi.ruleTaskTimeout) + mockedRulesData.filter( + (data) => data.executionStatus.lastDuration > parseDuration(ruleTypeFromApi.ruleTaskTimeout) ).length ); // Duration tooltip - wrapper - .find('[data-test-subj="alertsTableCell-durationTooltip"]') - .first() - .simulate('mouseOver'); + wrapper.find('[data-test-subj="rulesTableCell-durationTooltip"]').first().simulate('mouseOver'); // Run the timers so the EuiTooltip will be visible jest.runAllTimers(); @@ -464,37 +460,37 @@ describe('alerts_list component with items', () => { ); // Status column - expect(wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-status"]').length).toEqual( - mockedAlertsData.length + expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length).toEqual( + mockedRulesData.length ); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-active"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-ok"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-pending"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').length).toEqual(2); - expect(wrapper.find('[data-test-subj="alertStatus-error-tooltip"]').length).toEqual(2); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); + expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); expect( - wrapper.find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]').length + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length ).toEqual(1); - expect(wrapper.find('[data-test-subj="refreshAlertsButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').first().text()).toEqual( + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( 'Error' ); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').last().text()).toEqual( + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( 'License Error' ); // Monitoring column expect( - wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-successRatio"]').length - ).toEqual(mockedAlertsData.length); + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length + ).toEqual(mockedRulesData.length); const ratios = wrapper.find( - 'EuiTableRowCell[data-test-subj="alertsTableCell-successRatio"] span[data-test-subj="successRatio"]' + 'EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"] span[data-test-subj="successRatio"]' ); - mockedAlertsData.forEach((rule, index) => { + mockedRulesData.forEach((rule, index) => { if (rule.monitoring) { expect(ratios.at(index).text()).toEqual( `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` @@ -506,14 +502,14 @@ describe('alerts_list component with items', () => { // P50 column is rendered initially expect( - wrapper.find(`[data-test-subj="alertsTable-${Percentiles.P50}ColumnName"]`).exists() + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`).exists() ).toBeTruthy(); let percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` ); - mockedAlertsData.forEach((rule, index) => { + mockedRulesData.forEach((rule, index) => { if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { // Ensure the table cells are getting the correct values expect(percentiles.at(index).text()).toEqual( @@ -523,7 +519,7 @@ describe('alerts_list component with items', () => { expect( wrapper .find( - 'EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' + 'EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' ) .at(index) .props().content @@ -535,11 +531,11 @@ describe('alerts_list component with items', () => { // Click column to sort by P50 wrapper - .find(`[data-test-subj="alertsTable-${Percentiles.P50}ColumnName"]`) + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) .first() .simulate('click'); - expect(loadAlerts).toHaveBeenCalledWith( + expect(loadRules).toHaveBeenCalledWith( expect.objectContaining({ sort: { field: percentileFields[Percentiles.P50], @@ -550,11 +546,11 @@ describe('alerts_list component with items', () => { // Click column again to reverse sort by P50 wrapper - .find(`[data-test-subj="alertsTable-${Percentiles.P50}ColumnName"]`) + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) .first() .simulate('click'); - expect(loadAlerts).toHaveBeenCalledWith( + expect(loadRules).toHaveBeenCalledWith( expect.objectContaining({ sort: { field: percentileFields[Percentiles.P50], @@ -589,14 +585,14 @@ describe('alerts_list component with items', () => { wrapper.update(); expect( - wrapper.find(`[data-test-subj="alertsTable-${Percentiles.P95}ColumnName"]`).exists() + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`).exists() ).toBeTruthy(); percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` ); - mockedAlertsData.forEach((rule, index) => { + mockedRulesData.forEach((rule, index) => { if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { expect(percentiles.at(index).text()).toEqual( getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) @@ -608,11 +604,11 @@ describe('alerts_list component with items', () => { // Click column to sort by P95 wrapper - .find(`[data-test-subj="alertsTable-${Percentiles.P95}ColumnName"]`) + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) .first() .simulate('click'); - expect(loadAlerts).toHaveBeenCalledWith( + expect(loadRules).toHaveBeenCalledWith( expect.objectContaining({ sort: { field: percentileFields[Percentiles.P95], @@ -623,11 +619,11 @@ describe('alerts_list component with items', () => { // Click column again to reverse sort by P95 wrapper - .find(`[data-test-subj="alertsTable-${Percentiles.P95}ColumnName"]`) + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) .first() .simulate('click'); - expect(loadAlerts).toHaveBeenCalledWith( + expect(loadRules).toHaveBeenCalledWith( expect.objectContaining({ sort: { field: percentileFields[Percentiles.P95], @@ -640,16 +636,16 @@ describe('alerts_list component with items', () => { jest.clearAllMocks(); }); - it('loads alerts when refresh button is clicked', async () => { + it('loads rules when refresh button is clicked', async () => { await setup(); - wrapper.find('[data-test-subj="refreshAlertsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); await act(async () => { await nextTick(); wrapper.update(); }); - expect(loadAlerts).toHaveBeenCalled(); + expect(loadRules).toHaveBeenCalled(); }); it('renders license errors and manage license modal on click', async () => { @@ -657,11 +653,9 @@ describe('alerts_list component with items', () => { await setup(); expect(wrapper.find('ManageLicenseModal').exists()).toBeFalsy(); expect( - wrapper.find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]').length + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length ).toEqual(1); - wrapper - .find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]') - .simulate('click'); + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').simulate('click'); await act(async () => { await nextTick(); @@ -676,7 +670,7 @@ describe('alerts_list component with items', () => { expect(global.open).toHaveBeenCalled(); }); - it('sorts alerts when clicking the name column', async () => { + it('sorts rules when clicking the name column', async () => { await setup(); wrapper .find('[data-test-subj="tableHeaderCell_name_1"] .euiTableHeaderButton') @@ -688,7 +682,7 @@ describe('alerts_list component with items', () => { wrapper.update(); }); - expect(loadAlerts).toHaveBeenCalledWith( + expect(loadRules).toHaveBeenCalledWith( expect.objectContaining({ sort: { field: 'name', @@ -698,7 +692,7 @@ describe('alerts_list component with items', () => { ); }); - it('sorts alerts when clicking the enabled column', async () => { + it('sorts rules when clicking the enabled column', async () => { await setup(); wrapper .find('[data-test-subj="tableHeaderCell_enabled_0"] .euiTableHeaderButton') @@ -710,7 +704,7 @@ describe('alerts_list component with items', () => { wrapper.update(); }); - expect(loadAlerts).toHaveBeenLastCalledWith( + expect(loadRules).toHaveBeenLastCalledWith( expect.objectContaining({ sort: { field: 'enabled', @@ -722,22 +716,22 @@ describe('alerts_list component with items', () => { it('renders edit and delete buttons when user can manage rules', async () => { await setup(); - expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); }); it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { await setup(false); - expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); }); }); -describe('alerts_list component empty with show only capability', () => { +describe('rules_list component empty with show only capability', () => { let wrapper: ReactWrapper; async function setup() { - loadAlerts.mockResolvedValue({ + loadRules.mockResolvedValue({ page: 1, perPage: 10000, total: 0, @@ -753,8 +747,8 @@ describe('alerts_list component empty with show only capability', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([ - { id: 'test_alert_type', name: 'some alert type', authorizedConsumers: {} }, + loadRuleTypes.mockResolvedValue([ + { id: 'test_rule_type', name: 'some rule type', authorizedConsumers: {} }, ]); loadAllActions.mockResolvedValue([]); // eslint-disable-next-line react-hooks/rules-of-hooks @@ -762,7 +756,7 @@ describe('alerts_list component empty with show only capability', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); await act(async () => { await nextTick(); @@ -770,30 +764,30 @@ describe('alerts_list component empty with show only capability', () => { }); } - it('not renders create alert button', async () => { + it('not renders create rule button', async () => { await setup(); - expect(wrapper.find('[data-test-subj="createAlertButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="createRuleButton"]')).toHaveLength(0); }); }); -describe('alerts_list with show only capability', () => { +describe('rules_list with show only capability', () => { let wrapper: ReactWrapper; async function setup(editable: boolean = true) { - loadAlerts.mockResolvedValue({ + loadRules.mockResolvedValue({ page: 1, perPage: 10000, total: 2, data: [ { id: '1', - name: 'test alert', + name: 'test rule', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -809,13 +803,13 @@ describe('alerts_list with show only capability', () => { }, { id: '2', - name: 'test alert 2', + name: 'test rule 2', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], - params: { name: 'test alert type name' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -842,13 +836,13 @@ describe('alerts_list with show only capability', () => { }, ]); - loadAlertTypes.mockResolvedValue([alertTypeFromApi]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); loadAllActions.mockResolvedValue([]); const ruleTypeMock: RuleTypeModel = { - id: 'test_alert_type', + id: 'test_rule_type', iconClass: 'test', - description: 'Alert when testing', + description: 'Rule when testing', documentationUrl: 'https://localhost.local/docs', validate: () => { return { errors: {} }; @@ -864,7 +858,7 @@ describe('alerts_list with show only capability', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); await act(async () => { await nextTick(); @@ -872,14 +866,14 @@ describe('alerts_list with show only capability', () => { }); } - it('renders table of alerts with edit button disabled', async () => { + it('renders table of rules with edit button disabled', async () => { await setup(false); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); }); - it('renders table of alerts with delete button disabled', async () => { + it('renders table of rules with delete button disabled', async () => { const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); hasAllPrivilege.mockReturnValue(false); await setup(false); @@ -888,7 +882,7 @@ describe('alerts_list with show only capability', () => { expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); }); - it('renders table of alerts with actions menu collapsedItemActions', async () => { + it('renders table of rules with actions menu collapsedItemActions', async () => { await setup(false); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); @@ -896,24 +890,24 @@ describe('alerts_list with show only capability', () => { }); }); -describe('alerts_list with disabled itmes', () => { +describe('rules_list with disabled itmes', () => { let wrapper: ReactWrapper; async function setup() { - loadAlerts.mockResolvedValue({ + loadRules.mockResolvedValue({ page: 1, perPage: 10000, total: 2, data: [ { id: '1', - name: 'test alert', + name: 'test rule', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type', + ruleTypeId: 'test_rule_type', schedule: { interval: '5d' }, actions: [], - params: { name: 'test alert type name' }, + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -929,13 +923,13 @@ describe('alerts_list with disabled itmes', () => { }, { id: '2', - name: 'test alert 2', + name: 'test rule 2', tags: ['tag1'], enabled: true, - alertTypeId: 'test_alert_type_disabled_by_license', + ruleTypeId: 'test_rule_type_disabled_by_license', schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], - params: { name: 'test alert type name' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, scheduledTaskId: null, createdBy: null, updatedBy: null, @@ -962,11 +956,11 @@ describe('alerts_list with disabled itmes', () => { }, ]); - loadAlertTypes.mockResolvedValue([ - alertTypeFromApi, + loadRuleTypes.mockResolvedValue([ + ruleTypeFromApi, { - id: 'test_alert_type_disabled_by_license', - name: 'some alert type that is not allowed', + id: 'test_rule_type_disabled_by_license', + name: 'some rule type that is not allowed', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, actionVariables: { context: [], state: [] }, @@ -987,7 +981,7 @@ describe('alerts_list with disabled itmes', () => { // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); await act(async () => { await nextTick(); @@ -1001,7 +995,7 @@ describe('alerts_list with disabled itmes', () => { expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( - 'actAlertsList__tableRowDisabled' + 'actRulesList__tableRowDisabled' ); expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( 1 diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx similarity index 65% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 72228c285238dc..4aa98895d5d973 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -43,27 +43,27 @@ import { isEmpty } from 'lodash'; import { ActionType, Rule, - AlertTableItem, + RuleTableItem, RuleType, RuleTypeIndex, Pagination, Percentiles, } from '../../../../types'; -import { AlertAdd, AlertEdit } from '../../alert_form'; +import { RuleAdd, RuleEdit } from '../../rule_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; -import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; +import { RuleQuickEditButtonsWithApi as RuleQuickEditButtons } from '../../common/components/rule_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; -import { AlertStatusFilter, getHealthColor } from './alert_status_filter'; +import { RuleStatusFilter, getHealthColor } from './rule_status_filter'; import { - loadAlerts, - loadAlertAggregations, - loadAlertTypes, - disableAlert, - enableAlert, - deleteAlerts, -} from '../../../lib/alert_api'; + loadRules, + loadRuleAggregations, + loadRuleTypes, + disableRule, + enableRule, + deleteRules, +} from '../../../lib/rule_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; @@ -77,13 +77,13 @@ import { formatDuration, MONITORING_HISTORY_LIMIT, } from '../../../../../../alerting/common'; -import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; +import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; -import './alerts_list.scss'; +import './rules_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { ManageLicenseModal } from './manage_license_modal'; -import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; +import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled'; import { RuleEnabledSwitch } from './rule_enabled_switch'; import { PercentileSelectablePopover } from './percentile_selectable_popover'; import { RuleDurationFormat } from './rule_duration_format'; @@ -92,12 +92,12 @@ import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; const ENTER_KEY = 13; -interface AlertTypeState { +interface RuleTypeState { isLoading: boolean; isInitialized: boolean; data: RuleTypeIndex; } -interface AlertState { +interface RuleState { isLoading: boolean; data: Rule[]; totalItemCount: number; @@ -121,7 +121,7 @@ const initialPercentileOptions = Object.values(Percentiles).map((percentile) => key: percentile, })); -export const AlertsList: React.FunctionComponent = () => { +export const RulesList: React.FunctionComponent = () => { const history = useHistory(); const { http, @@ -143,11 +143,11 @@ export const AlertsList: React.FunctionComponent = () => { const [inputText, setInputText] = useState(); const [typesFilter, setTypesFilter] = useState([]); const [actionTypesFilter, setActionTypesFilter] = useState([]); - const [alertStatusesFilter, setAlertStatusesFilter] = useState([]); - const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); - const [dismissAlertErrors, setDismissAlertErrors] = useState(false); + const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); + const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); + const [dismissRuleErrors, setDismissRuleErrors] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); - const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); const [percentileOptions, setPercentileOptions] = @@ -160,15 +160,15 @@ export const AlertsList: React.FunctionComponent = () => { } }, [percentileOptions]); - const [sort, setSort] = useState['sort']>({ + const [sort, setSort] = useState['sort']>({ field: 'name', direction: 'asc', }); const [manageLicenseModalOpts, setManageLicenseModalOpts] = useState<{ licenseType: string; - alertTypeId: string; + ruleTypeId: string; } | null>(null); - const [alertsStatusesTotal, setAlertsStatusesTotal] = useState>( + const [rulesStatusesTotal, setRulesStatusesTotal] = useState>( AlertExecutionStatusValues.reduce( (prev: Record, status: string) => ({ @@ -178,18 +178,18 @@ export const AlertsList: React.FunctionComponent = () => { {} ) ); - const [alertTypesState, setAlertTypesState] = useState({ + const [ruleTypesState, setRuleTypesState] = useState({ isLoading: false, isInitialized: false, data: new Map(), }); - const [alertsState, setAlertsState] = useState({ + const [rulesState, setRulesState] = useState({ isLoading: false, data: [], totalItemCount: 0, }); - const [alertsToDelete, setAlertsToDelete] = useState([]); - const onRuleEdit = (ruleItem: AlertTableItem) => { + const [rulesToDelete, setRulesToDelete] = useState([]); + const onRuleEdit = (ruleItem: RuleTableItem) => { setEditFlyoutVisibility(true); setCurrentRuleToEdit(ruleItem); }; @@ -198,35 +198,35 @@ export const AlertsList: React.FunctionComponent = () => { ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; useEffect(() => { - loadAlertsData(); + loadRulesData(); }, [ - alertTypesState, + ruleTypesState, page, searchText, percentileOptions, JSON.stringify(typesFilter), JSON.stringify(actionTypesFilter), - JSON.stringify(alertStatusesFilter), + JSON.stringify(ruleStatusesFilter), ]); useEffect(() => { (async () => { try { - setAlertTypesState({ ...alertTypesState, isLoading: true }); - const alertTypes = await loadAlertTypes({ http }); + setRuleTypesState({ ...ruleTypesState, isLoading: true }); + const ruleTypes = await loadRuleTypes({ http }); const index: RuleTypeIndex = new Map(); - for (const alertType of alertTypes) { - index.set(alertType.id, alertType); + for (const ruleType of ruleTypes) { + index.set(ruleType.id, ruleType); } - setAlertTypesState({ isLoading: false, data: index, isInitialized: true }); + setRuleTypesState({ isLoading: false, data: index, isInitialized: true }); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleTypesMessage', + 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage', { defaultMessage: 'Unable to load rule types' } ), }); - setAlertTypesState({ ...alertTypesState, isLoading: false }); + setRuleTypesState({ ...ruleTypesState, isLoading: false }); } })(); }, []); @@ -246,7 +246,7 @@ export const AlertsList: React.FunctionComponent = () => { } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage', + 'xpack.triggersActionsUI.sections.rulesList.unableToLoadConnectorTypesMessage', { defaultMessage: 'Unable to load connector types' } ), }); @@ -254,29 +254,28 @@ export const AlertsList: React.FunctionComponent = () => { })(); }, []); - async function loadAlertsData() { - const hasAnyAuthorizedAlertType = - alertTypesState.isInitialized && alertTypesState.data.size > 0; - if (hasAnyAuthorizedAlertType) { - setAlertsState({ ...alertsState, isLoading: true }); + async function loadRulesData() { + const hasAnyAuthorizedRuleType = ruleTypesState.isInitialized && ruleTypesState.data.size > 0; + if (hasAnyAuthorizedRuleType) { + setRulesState({ ...rulesState, isLoading: true }); try { - const alertsResponse = await loadAlerts({ + const rulesResponse = await loadRules({ http, page, searchText, typesFilter, actionTypesFilter, - alertStatusesFilter, + ruleStatusesFilter, sort, }); - await loadAlertAggs(); - setAlertsState({ + await loadRuleAggs(); + setRulesState({ isLoading: false, - data: alertsResponse.data, - totalItemCount: alertsResponse.total, + data: rulesResponse.data, + totalItemCount: rulesResponse.total, }); - if (!alertsResponse.data?.length && page.index > 0) { + if (!rulesResponse.data?.length && page.index > 0) { setPage({ ...page, index: 0 }); } @@ -284,41 +283,41 @@ export const AlertsList: React.FunctionComponent = () => { isEmpty(searchText) && isEmpty(typesFilter) && isEmpty(actionTypesFilter) && - isEmpty(alertStatusesFilter) + isEmpty(ruleStatusesFilter) ); - setNoData(alertsResponse.data.length === 0 && !isFilterApplied); + setNoData(rulesResponse.data.length === 0 && !isFilterApplied); } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage', + 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage', { defaultMessage: 'Unable to load rules', } ), }); - setAlertsState({ ...alertsState, isLoading: false }); + setRulesState({ ...rulesState, isLoading: false }); } setInitialLoad(false); } } - async function loadAlertAggs() { + async function loadRuleAggs() { try { - const alertsAggs = await loadAlertAggregations({ + const rulesAggs = await loadRuleAggregations({ http, searchText, typesFilter, actionTypesFilter, - alertStatusesFilter, + ruleStatusesFilter, }); - if (alertsAggs?.alertExecutionStatus) { - setAlertsStatusesTotal(alertsAggs.alertExecutionStatus); + if (rulesAggs?.ruleExecutionStatus) { + setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); } } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.unableToLoadRuleStatusInfoMessage', + 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage', { defaultMessage: 'Unable to load rule status info', } @@ -329,7 +328,7 @@ export const AlertsList: React.FunctionComponent = () => { const renderAlertExecutionStatus = ( executionStatus: AlertExecutionStatus, - item: AlertTableItem + item: RuleTableItem ) => { const healthColor = getHealthColor(executionStatus.status); const tooltipMessage = @@ -338,20 +337,16 @@ export const AlertsList: React.FunctionComponent = () => { executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; const statusMessage = isLicenseError ? ALERT_STATUS_LICENSE_ERROR - : alertsStatusesTranslationsMapping[executionStatus.status]; + : rulesStatusesTranslationsMapping[executionStatus.status]; const health = ( - + {statusMessage} ); const healthWithTooltip = tooltipMessage ? ( - + {health} ) : ( @@ -365,16 +360,16 @@ export const AlertsList: React.FunctionComponent = () => { setManageLicenseModalOpts({ - licenseType: alertTypesState.data.get(item.alertTypeId)?.minimumLicenseRequired!, - alertTypeId: item.alertTypeId, + licenseType: ruleTypesState.data.get(item.ruleTypeId)?.minimumLicenseRequired!, + ruleTypeId: item.ruleTypeId, }) } > @@ -386,10 +381,10 @@ export const AlertsList: React.FunctionComponent = () => { const renderPercentileColumnName = () => { return ( - + { field: percentileFields[selectedPercentile!], width: '16%', name: renderPercentileColumnName(), - 'data-test-subj': 'alertsTableCell-ruleExecutionPercentile', + 'data-test-subj': 'rulesTableCell-ruleExecutionPercentile', sortable: true, truncateText: false, render: renderPercentileCellValue, }; }; - const getAlertsTableColumns = () => { + const getRulesTableColumns = () => { return [ { field: 'enabled', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle', { defaultMessage: 'Enabled' } ), width: '50px', - render(_enabled: boolean | undefined, item: AlertTableItem) { + render(_enabled: boolean | undefined, item: RuleTableItem) { return ( await disableAlert({ http, id: item.id })} - enableAlert={async () => await enableAlert({ http, id: item.id })} + disableRule={async () => await disableRule({ http, id: item.id })} + enableRule={async () => await enableRule({ http, id: item.id })} item={item} - onAlertChanged={() => loadAlertsData()} + onRuleChanged={() => loadRulesData()} /> ); }, sortable: true, - 'data-test-subj': 'alertsTableCell-enabled', + 'data-test-subj': 'rulesTableCell-enabled', }, { field: 'name', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle', { defaultMessage: 'Name' } ), sortable: true, truncateText: true, width: '30%', - 'data-test-subj': 'alertsTableCell-name', - render: (name: string, alert: AlertTableItem) => { - const ruleType = alertTypesState.data.get(alert.alertTypeId); - const checkEnabledResult = checkAlertTypeEnabled(ruleType); + 'data-test-subj': 'rulesTableCell-name', + render: (name: string, rule: RuleTableItem) => { + const ruleType = ruleTypesState.data.get(rule.ruleTypeId); + const checkEnabledResult = checkRuleTypeEnabled(ruleType); const link = ( <> @@ -477,7 +472,7 @@ export const AlertsList: React.FunctionComponent = () => { { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + history.push(routeToRuleDetails.replace(`:ruleId`, rule.id)); }} > {name} @@ -498,7 +493,7 @@ export const AlertsList: React.FunctionComponent = () => { - {alert.alertType} + {rule.ruleType}
@@ -507,10 +502,10 @@ export const AlertsList: React.FunctionComponent = () => { return ( <> {link} - {alert.enabled && alert.muteAll && ( + {rule.enabled && rule.muteAll && ( @@ -524,8 +519,8 @@ export const AlertsList: React.FunctionComponent = () => { name: '', sortable: false, width: '50px', - 'data-test-subj': 'alertsTableCell-tagsPopover', - render: (tags: string[], item: AlertTableItem) => { + 'data-test-subj': 'rulesTableCell-tagsPopover', + render: (tags: string[], item: RuleTableItem) => { return tags.length > 0 ? ( { field: 'executionStatus.lastExecutionDate', name: ( { ), sortable: true, width: '15%', - 'data-test-subj': 'alertsTableCell-lastExecutionDate', + 'data-test-subj': 'rulesTableCell-lastExecutionDate', render: (date: Date) => { if (date) { return ( @@ -608,12 +603,12 @@ export const AlertsList: React.FunctionComponent = () => { field: 'schedule.interval', width: '6%', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle', { defaultMessage: 'Interval' } ), sortable: false, truncateText: false, - 'data-test-subj': 'alertsTableCell-interval', + 'data-test-subj': 'rulesTableCell-interval', render: (interval: string) => formatDuration(interval), }, { @@ -621,9 +616,9 @@ export const AlertsList: React.FunctionComponent = () => { width: '12%', name: ( { ), sortable: true, truncateText: false, - 'data-test-subj': 'alertsTableCell-duration', - render: (value: number, item: AlertTableItem) => { + 'data-test-subj': 'rulesTableCell-duration', + render: (value: number, item: RuleTableItem) => { const showDurationWarning = shouldShowDurationWarning( - alertTypesState.data.get(item.alertTypeId), + ruleTypesState.data.get(item.ruleTypeId), value ); @@ -651,10 +646,10 @@ export const AlertsList: React.FunctionComponent = () => { { width: '12%', name: ( { ), sortable: true, truncateText: false, - 'data-test-subj': 'alertsTableCell-successRatio', + 'data-test-subj': 'rulesTableCell-successRatio', render: (value: number) => { return ( @@ -700,58 +695,58 @@ export const AlertsList: React.FunctionComponent = () => { { field: 'executionStatus.status', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle', { defaultMessage: 'Status' } ), sortable: true, truncateText: false, width: '120px', - 'data-test-subj': 'alertsTableCell-status', - render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + 'data-test-subj': 'rulesTableCell-status', + render: (_executionStatus: AlertExecutionStatus, item: RuleTableItem) => { return renderAlertExecutionStatus(item.executionStatus, item); }, }, { name: '', width: '10%', - render(item: AlertTableItem) { + render(item: RuleTableItem) { return ( - + - {item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) ? ( - + {item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId) ? ( + onRuleEdit(item)} iconType={'pencil'} aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel', { defaultMessage: 'Edit' } )} /> ) : null} {item.isEditable ? ( - + setAlertsToDelete([item.id])} + onClick={() => setRulesToDelete([item.id])} iconType={'trash'} aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel', + 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel', { defaultMessage: 'Delete' } )} /> @@ -763,9 +758,9 @@ export const AlertsList: React.FunctionComponent = () => { loadAlertsData()} - setAlertsToDelete={setAlertsToDelete} - onEditAlert={() => onRuleEdit(item)} + onRuleChanged={() => loadRulesData()} + setRulesToDelete={setRulesToDelete} + onEditRule={() => onRuleEdit(item)} /> @@ -775,17 +770,17 @@ export const AlertsList: React.FunctionComponent = () => { ]; }; - const authorizedAlertTypes = [...alertTypesState.data.values()]; - const authorizedToCreateAnyAlerts = authorizedAlertTypes.some( - (alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all + const authorizedRuleTypes = [...ruleTypesState.data.values()]; + const authorizedToCreateAnyRules = authorizedRuleTypes.some( + (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all ); const getProducerFeatureName = (producer: string) => { return kibanaFeatures?.find((featureItem) => featureItem.id === producer)?.name; }; - const groupAlertTypesByProducer = () => { - return authorizedAlertTypes.reduce( + const groupRuleTypesByProducer = () => { + return authorizedRuleTypes.reduce( ( result: Record< string, @@ -794,12 +789,12 @@ export const AlertsList: React.FunctionComponent = () => { name: string; }> >, - alertType + ruleType ) => { - const producer = alertType.producer; + const producer = ruleType.producer; (result[producer] = result[producer] || []).push({ - value: alertType.id, - name: alertType.name, + value: ruleType.id, + name: ruleType.name, }); return result; }, @@ -811,10 +806,10 @@ export const AlertsList: React.FunctionComponent = () => { setTypesFilter(types)} - options={sortBy(Object.entries(groupAlertTypesByProducer())).map( - ([groupName, alertTypesOptions]) => ({ + options={sortBy(Object.entries(groupRuleTypesByProducer())).map( + ([groupName, ruleTypesOptions]) => ({ groupName: getProducerFeatureName(groupName) ?? capitalize(groupName), - subOptions: alertTypesOptions.sort((a, b) => a.name.localeCompare(b.name)), + subOptions: ruleTypesOptions.sort((a, b) => a.name.localeCompare(b.name)), }) )} />, @@ -823,63 +818,63 @@ export const AlertsList: React.FunctionComponent = () => { actionTypes={actionTypes} onChange={(ids: string[]) => setActionTypesFilter(ids)} />, - setAlertStatusesFilter(ids)} + setRuleStatusesFilter(ids)} />, , ]; - const authorizedToModifySelectedAlerts = selectedIds.length - ? filterAlertsById(alertsState.data, selectedIds).every((selectedAlert) => - hasAllPrivilege(selectedAlert, alertTypesState.data.get(selectedAlert.alertTypeId)) + const authorizedToModifySelectedRules = selectedIds.length + ? filterRulesById(rulesState.data, selectedIds).every((selectedRule) => + hasAllPrivilege(selectedRule, ruleTypesState.data.get(selectedRule.ruleTypeId)) ) : false; const table = ( <> - {selectedIds.length > 0 && authorizedToModifySelectedAlerts && ( + {selectedIds.length > 0 && authorizedToModifySelectedRules && ( - setIsPerformingAction(true)} onActionPerformed={() => { - loadAlertsData(); + loadRulesData(); setIsPerformingAction(false); }} - setAlertsToDelete={setAlertsToDelete} + setRulesToDelete={setRulesToDelete} /> )} - {authorizedToCreateAnyAlerts ? ( + {authorizedToCreateAnyRules ? ( setAlertFlyoutVisibility(true)} + onClick={() => setRuleFlyoutVisibility(true)} > @@ -889,7 +884,7 @@ export const AlertsList: React.FunctionComponent = () => { { setInputText(e.target.value); if (e.target.value === '') { @@ -902,7 +897,7 @@ export const AlertsList: React.FunctionComponent = () => { } }} placeholder={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle', + 'xpack.triggersActionsUI.sections.rulesList.searchPlaceholderTitle', { defaultMessage: 'Search' } )} /> @@ -918,7 +913,7 @@ export const AlertsList: React.FunctionComponent = () => { - {!dismissAlertErrors && alertsStatusesTotal.error > 0 ? ( + {!dismissRuleErrors && rulesStatusesTotal.error > 0 ? ( { size="s" title={ } - iconType="alert" - data-test-subj="alertsErrorBanner" + iconType="rule" + data-test-subj="rulesErrorBanner" > setAlertStatusesFilter(['error'])} + onClick={() => setRuleStatusesFilter(['error'])} > - setDismissAlertErrors(true)}> + setDismissRuleErrors(true)}> @@ -960,64 +955,64 @@ export const AlertsList: React.FunctionComponent = () => { - + - + - + - + - + - + @@ -1026,38 +1021,38 @@ export const AlertsList: React.FunctionComponent = () => { ({ - 'data-test-subj': 'alert-row', - className: !alertTypesState.data.get(item.alertTypeId)?.enabledInLicense - ? 'actAlertsList__tableRowDisabled' + rowProps={(item: RuleTableItem) => ({ + 'data-test-subj': 'rule-row', + className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense + ? 'actRulesList__tableRowDisabled' : '', })} - cellProps={(item: AlertTableItem) => ({ + cellProps={(item: RuleTableItem) => ({ 'data-test-subj': 'cell', - className: !alertTypesState.data.get(item.alertTypeId)?.enabledInLicense - ? 'actAlertsList__tableCellDisabled' + className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense + ? 'actRulesList__tableCellDisabled' : '', })} - data-test-subj="alertsList" + data-test-subj="rulesList" pagination={{ pageIndex: page.index, pageSize: page.size, - /* Don't display alert count until we have the alert types initialized */ - totalItemCount: alertTypesState.isInitialized === false ? 0 : alertsState.totalItemCount, + /* Don't display rule count until we have the rule types initialized */ + totalItemCount: ruleTypesState.isInitialized === false ? 0 : rulesState.totalItemCount, }} selection={{ - selectable: (alert: AlertTableItem) => alert.isEditable, - onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { + selectable: (rule: RuleTableItem) => rule.isEditable, + onSelectionChange(updatedSelectedItemsList: RuleTableItem[]) { setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); }, }} @@ -1066,7 +1061,7 @@ export const AlertsList: React.FunctionComponent = () => { sort: changedSort, }: { page?: Pagination; - sort?: EuiTableSortingType['sort']; + sort?: EuiTableSortingType['sort']; }) => { if (changedPage) { setPage(changedPage); @@ -1079,7 +1074,7 @@ export const AlertsList: React.FunctionComponent = () => { {manageLicenseModalOpts && ( { window.open(`${http.basePath.get()}/app/management/stack/license_management`, '_blank'); setManageLicenseModalOpts(null); @@ -1092,9 +1087,9 @@ export const AlertsList: React.FunctionComponent = () => { // if initial load, show spinner const getRulesList = () => { - if (noData && !alertsState.isLoading && !alertTypesState.isLoading) { - return authorizedToCreateAnyAlerts ? ( - setAlertFlyoutVisibility(true)} /> + if (noData && !rulesState.isLoading && !ruleTypesState.isLoading) { + return authorizedToCreateAnyRules ? ( + setRuleFlyoutVisibility(true)} /> ) : ( noPermissionPrompt ); @@ -1108,59 +1103,59 @@ export const AlertsList: React.FunctionComponent = () => { }; return ( -
+
{ - setAlertsToDelete([]); + setRulesToDelete([]); setSelectedIds([]); - await loadAlertsData(); + await loadRulesData(); }} onErrors={async () => { - // Refresh the alerts from the server, some alerts may have beend deleted - await loadAlertsData(); - setAlertsToDelete([]); + // Refresh the rules from the server, some rules may have beend deleted + await loadRulesData(); + setRulesToDelete([]); }} onCancel={() => { - setAlertsToDelete([]); + setRulesToDelete([]); }} - apiDeleteCall={deleteAlerts} - idsToDelete={alertsToDelete} - singleTitle={i18n.translate('xpack.triggersActionsUI.sections.alertsList.singleTitle', { + apiDeleteCall={deleteRules} + idsToDelete={rulesToDelete} + singleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.singleTitle', { defaultMessage: 'rule', })} - multipleTitle={i18n.translate('xpack.triggersActionsUI.sections.alertsList.multipleTitle', { + multipleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.multipleTitle', { defaultMessage: 'rules', })} setIsLoadingState={(isLoading: boolean) => { - setAlertsState({ ...alertsState, isLoading }); + setRulesState({ ...rulesState, isLoading }); }} /> {getRulesList()} - {alertFlyoutVisible && ( - { - setAlertFlyoutVisibility(false); + setRuleFlyoutVisibility(false); }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} - ruleTypeIndex={alertTypesState.data} - onSave={loadAlertsData} + ruleTypeIndex={ruleTypesState.data} + onSave={loadRulesData} /> )} {editFlyoutVisible && currentRuleToEdit && ( - { setEditFlyoutVisibility(false); }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} ruleType={ - alertTypesState.data.get(currentRuleToEdit.alertTypeId) as RuleType + ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType } - onSave={loadAlertsData} + onSave={loadRulesData} /> )}
@@ -1168,7 +1163,7 @@ export const AlertsList: React.FunctionComponent = () => { }; // eslint-disable-next-line import/no-default-export -export { AlertsList as default }; +export { RulesList as default }; const noPermissionPrompt = (
@@ -1184,7 +1179,7 @@ const noPermissionPrompt = ( body={

@@ -1192,23 +1187,23 @@ const noPermissionPrompt = ( /> ); -function filterAlertsById(alerts: Rule[], ids: string[]): Rule[] { - return alerts.filter((alert) => ids.includes(alert.id)); +function filterRulesById(rules: Rule[], ids: string[]): Rule[] { + return rules.filter((rule) => ids.includes(rule.id)); } -function convertAlertsToTableItems( - alerts: Rule[], +function convertRulesToTableItems( + rules: Rule[], ruleTypeIndex: RuleTypeIndex, canExecuteActions: boolean ) { - return alerts.map((alert, index: number) => ({ - ...alert, + return rules.map((rule, index: number) => ({ + ...rule, index, - actionsCount: alert.actions.length, - alertType: ruleTypeIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, + actionsCount: rule.actions.length, + ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId, isEditable: - hasAllPrivilege(alert, ruleTypeIndex.get(alert.alertTypeId)) && - (canExecuteActions || (!canExecuteActions && !alert.actions.length)), - enabledInLicense: !!ruleTypeIndex.get(alert.alertTypeId)?.enabledInLicense, + hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) && + (canExecuteActions || (!canExecuteActions && !rule.actions.length)), + enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense, })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx similarity index 90% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/type_filter.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx index 3351e903f7bcdb..6ce697f65f8980 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/type_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx @@ -52,10 +52,10 @@ export const TypeFilter: React.FunctionComponent = ({ numActiveFilters={selectedValues.length} numFilters={selectedValues.length} onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="alertTypeFilterButton" + data-test-subj="ruleTypeFilterButton" > @@ -64,7 +64,7 @@ export const TypeFilter: React.FunctionComponent = ({
{options.map((groupItem, groupIndex) => ( - +

{groupItem.groupName}

{groupItem.subOptions.map((item, index) => ( @@ -79,7 +79,7 @@ export const TypeFilter: React.FunctionComponent = ({ } }} checked={selectedValues.includes(item.value) ? 'on' : undefined} - data-test-subj={`alertType${item.value}FilterOption`} + data-test-subj={`ruleType${item.value}FilterOption`} > {item.name} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts similarity index 68% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts index 293a5e79e29b21..cccfe6074b7c16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/translations.ts @@ -8,48 +8,48 @@ import { i18n } from '@kbn/i18n'; export const ALERT_STATUS_OK = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusOk', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusOk', { defaultMessage: 'Ok', } ); export const ALERT_STATUS_ACTIVE = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusActive', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusActive', { defaultMessage: 'Active', } ); export const ALERT_STATUS_ERROR = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusError', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusError', { defaultMessage: 'Error', } ); export const ALERT_STATUS_LICENSE_ERROR = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusLicenseError', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError', { defaultMessage: 'License Error', } ); export const ALERT_STATUS_PENDING = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusPending', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusPending', { defaultMessage: 'Pending', } ); export const ALERT_STATUS_UNKNOWN = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown', + 'xpack.triggersActionsUI.sections.rulesList.ruleStatusUnknown', { defaultMessage: 'Unknown', } ); -export const alertsStatusesTranslationsMapping = { +export const rulesStatusesTranslationsMapping = { ok: ALERT_STATUS_OK, active: ALERT_STATUS_ACTIVE, error: ALERT_STATUS_ERROR, @@ -58,55 +58,55 @@ export const alertsStatusesTranslationsMapping = { }; export const ALERT_ERROR_UNKNOWN_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonUnknown', { defaultMessage: 'An error occurred for unknown reasons.', } ); export const ALERT_ERROR_READING_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonReading', { defaultMessage: 'An error occurred when reading the rule.', } ); export const ALERT_ERROR_DECRYPTING_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonDecrypting', { defaultMessage: 'An error occurred when decrypting the rule.', } ); export const ALERT_ERROR_EXECUTION_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonRunning', { defaultMessage: 'An error occurred when running the rule.', } ); export const ALERT_ERROR_LICENSE_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonLicense', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonLicense', { defaultMessage: 'Cannot run rule', } ); export const ALERT_ERROR_TIMEOUT_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonTimeout', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonTimeout', { defaultMessage: 'Rule execution cancelled due to timeout.', } ); export const ALERT_ERROR_DISABLED_REASON = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDisabled', + 'xpack.triggersActionsUI.sections.rulesList.ruleErrorReasonDisabled', { defaultMessage: 'Rule failed to execute because rule ran after it was disabled.', } ); -export const alertsErrorReasonTranslationsMapping = { +export const rulesErrorReasonTranslationsMapping = { read: ALERT_ERROR_READING_REASON, decrypt: ALERT_ERROR_DECRYPTING_REASON, execute: ALERT_ERROR_EXECUTION_REASON, diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx index 2698f4ee2e428e..85735a7854d3f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { AlertAdd } from '../application/sections/alert_form'; -import type { AlertAddProps } from '../types'; +import { RuleAdd } from '../application/sections/rule_form'; +import type { RuleAddProps as AlertAddProps } from '../types'; export const getAddAlertFlyoutLazy = (props: AlertAddProps) => { - return ; + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx index 26cc1159e5afdd..58bdc43e15377a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { AlertEdit } from '../application/sections/alert_form'; -import type { AlertEditProps } from '../types'; +import { RuleEdit } from '../application/sections/rule_form'; +import type { RuleEditProps as AlertEditProps } from '../types'; export const getEditAlertFlyoutLazy = (props: AlertEditProps) => { - return ; + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts index e9f6a2a38b5136..072684de68b3ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts @@ -91,7 +91,7 @@ export const getFields = async (http: HttpSetup, indexes: string[]) => { export const firstFieldOption = { text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel', + 'xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel', { defaultMessage: 'Select a field', } diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts index 74c6fe76ba2897..247ac03cd9149b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/health_api.ts @@ -9,6 +9,26 @@ import { HttpSetup } from 'kibana/public'; const TRIGGERS_ACTIONS_UI_API_ROOT = '/api/triggers_actions_ui'; -export async function triggersActionsUiHealth({ http }: { http: HttpSetup }): Promise { - return await http.get(`${TRIGGERS_ACTIONS_UI_API_ROOT}/_health`); +interface TriggersActionsUiHealth { + isRulesAvailable: boolean; +} + +interface TriggersActionsServerHealth { + isAlertsAvailable: boolean; +} + +export async function triggersActionsUiHealth({ + http, +}: { + http: HttpSetup; +}): Promise { + const result = await http.get( + `${TRIGGERS_ACTIONS_UI_API_ROOT}/_health` + ); + if (result) { + return { + isRulesAvailable: result.isAlertsAvailable, + }; + } + return result; } diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 67b1518a4b8e25..36ac247cf5c7ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -11,7 +11,7 @@ import { Plugin } from './plugin'; export type { - AlertAction, + RuleAction, Rule, RuleTypeModel, ActionType, @@ -22,7 +22,7 @@ export type { ActionVariables, ActionConnector, IErrorObject, - AlertFlyoutCloseReason, + RuleFlyoutCloseReason, RuleTypeParams, AsApiContract, } from './types'; @@ -45,7 +45,7 @@ export function plugin() { export { Plugin }; export * from './plugin'; -export { loadAlertAggregations } from './application/lib/alert_api/aggregate'; +export { loadRuleAggregations } from './application/lib/rule_api/aggregate'; export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 0ef528ed921a36..7a0420594118bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -15,8 +15,8 @@ import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { TypeRegistry } from './application/type_registry'; import { ActionTypeModel, - AlertAddProps, - AlertEditProps, + RuleAddProps, + RuleEditProps, RuleTypeModel, ConnectorAddFlyoutProps, ConnectorEditFlyoutProps, @@ -37,16 +37,14 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { actionTypeRegistry, }); }, - getAddAlertFlyout: (props: Omit) => { + getAddAlertFlyout: (props: Omit) => { return getAddAlertFlyoutLazy({ ...props, actionTypeRegistry, ruleTypeRegistry, }); }, - getEditAlertFlyout: ( - props: Omit - ) => { + getEditAlertFlyout: (props: Omit) => { return getEditAlertFlyoutLazy({ ...props, actionTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 12ea42b6c6bf09..e11a1d7e61a1d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -34,8 +34,8 @@ import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import type { ActionTypeModel, - AlertAddProps, - AlertEditProps, + RuleAddProps, + RuleEditProps, RuleTypeModel, ConnectorAddFlyoutProps, ConnectorEditFlyoutProps, @@ -56,11 +56,11 @@ export interface TriggersAndActionsUIPublicPluginStart { props: Omit ) => ReactElement; getAddAlertFlyout: ( - props: Omit - ) => ReactElement; + props: Omit + ) => ReactElement; getEditAlertFlyout: ( - props: Omit - ) => ReactElement; + props: Omit + ) => ReactElement; } interface PluginsSetup { @@ -186,9 +186,7 @@ export class Plugin actionTypeRegistry: this.actionTypeRegistry, }); }, - getAddAlertFlyout: ( - props: Omit - ) => { + getAddAlertFlyout: (props: Omit) => { return getAddAlertFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, @@ -196,7 +194,7 @@ export class Plugin }); }, getEditAlertFlyout: ( - props: Omit + props: Omit ) => { return getEditAlertFlyoutLazy({ ...props, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 67c2da627bf396..ef2f02411de384 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -23,17 +23,17 @@ import { TypeRegistry } from './application/type_registry'; import { ActionGroup, AlertActionParam, - SanitizedAlert as SanitizedRule, + SanitizedAlert, ResolvedSanitizedRule, - AlertAction, + AlertAction as RuleAction, AlertAggregations, RuleTaskState, - AlertSummary, + AlertSummary as RuleSummary, ExecutionDuration, AlertStatus, RawAlertInstance, AlertingFrameworkHealth, - AlertNotifyWhenType, + AlertNotifyWhenType as RuleNotifyWhenType, AlertTypeParams as RuleTypeParams, ActionVariable, RuleType as CommonRuleType, @@ -41,22 +41,34 @@ import { // In Triggers and Actions we treat all `Alert`s as `SanitizedRule` // so the `Params` is a black-box of Record -type Rule = SanitizedRule; -type ResolvedRule = ResolvedSanitizedRule; +type SanitizedRule = Omit< + SanitizedAlert, + 'alertTypeId' +> & { + ruleTypeId: SanitizedAlert['alertTypeId']; +}; +type Rule = SanitizedRule; +type ResolvedRule = Omit, 'alertTypeId'> & { + ruleTypeId: ResolvedSanitizedRule['alertTypeId']; +}; +type RuleAggregations = Omit & { + ruleExecutionStatus: AlertAggregations['alertExecutionStatus']; +}; export type { Rule, - AlertAction, - AlertAggregations, + RuleAction, + RuleAggregations, RuleTaskState, - AlertSummary, + RuleSummary, ExecutionDuration, AlertStatus, RawAlertInstance, AlertingFrameworkHealth, - AlertNotifyWhenType, + RuleNotifyWhenType, RuleTypeParams, ResolvedRule, + SanitizedRule, }; export type { ActionType, AsApiContract }; export { @@ -93,7 +105,7 @@ export interface ActionConnectorFieldsProps { isEdit: boolean; } -export enum AlertFlyoutCloseReason { +export enum RuleFlyoutCloseReason { SAVED, CANCELED, } @@ -232,10 +244,10 @@ export interface RuleType< export type SanitizedRuleType = Omit; -export type AlertUpdates = Omit; +export type RuleUpdates = Omit; -export interface AlertTableItem extends Rule { - alertType: RuleType['name']; +export interface RuleTableItem extends Rule { + ruleType: RuleType['name']; index: number; actionsCount: number; isEditable: boolean; @@ -250,7 +262,7 @@ export interface RuleTypeParamsExpressionProps< ruleParams: Params; ruleInterval: string; ruleThrottle: string; - alertNotifyWhen: AlertNotifyWhenType; + alertNotifyWhen: RuleNotifyWhenType; setRuleParams: (property: Key, value: Params[Key] | undefined) => void; setRuleProperty: ( key: Prop, @@ -303,28 +315,28 @@ export interface ConnectorEditFlyoutProps { actionTypeRegistry: ActionTypeRegistryContract; } -export interface AlertEditProps> { - initialAlert: Rule; +export interface RuleEditProps> { + initialRule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; - onClose: (reason: AlertFlyoutCloseReason) => void; + onClose: (reason: RuleFlyoutCloseReason) => void; /** @deprecated use `onSave` as a callback after an alert is saved*/ - reloadAlerts?: () => Promise; + reloadRules?: () => Promise; onSave?: () => Promise; metadata?: MetaData; ruleType?: RuleType; } -export interface AlertAddProps> { +export interface RuleAddProps> { consumer: string; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; - onClose: (reason: AlertFlyoutCloseReason) => void; - alertTypeId?: string; + onClose: (reason: RuleFlyoutCloseReason) => void; + ruleTypeId?: string; canChangeTrigger?: boolean; initialValues?: Partial; /** @deprecated use `onSave` as a callback after an alert is saved*/ - reloadAlerts?: () => Promise; + reloadRules?: () => Promise; onSave?: () => Promise; metadata?: MetaData; ruleTypeIndex?: RuleTypeIndex; diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index ac36780f10c018..38d3fa9ad59960 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -17,6 +17,7 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../features/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 0c31a5b8d2fe51..69584dd75de7d7 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -36,11 +36,11 @@ describe('transformFlatSettings', () => { transformFlatSettings({ settings: { // Settings that should get preserved + // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.number_of_replicas': '1', 'index.number_of_shards': '5', // Blacklisted settings - // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.allocation.existing_shards_allocator': 'gateway_allocator', 'index.blocks.write': 'true', 'index.creation_date': '1547052614626', @@ -87,11 +87,11 @@ describe('transformFlatSettings', () => { transformFlatSettings({ settings: { // Settings that should get preserved + // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.number_of_replicas': '1', 'index.number_of_shards': '5', // Deprecated settings - // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.soft_deletes.enabled': 'true', 'index.translog.retention.size': '5b', }, @@ -111,11 +111,11 @@ describe('transformFlatSettings', () => { transformFlatSettings({ settings: { // Settings that should get preserved + // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.number_of_replicas': '1', 'index.number_of_shards': '5', // Deprecated settings - // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.soft_deletes.enabled': 'true', 'index.translog.retention.age': '5d', }, @@ -245,6 +245,7 @@ describe('transformFlatSettings', () => { expect( getReindexWarnings({ settings: { + // @ts-expect-error @elastic/elasticsearch doesn't declare it 'index.number_of_replicas': '1', }, mappings: {}, diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts index b757602c6ab530..6dffead8ec91f6 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts @@ -24,7 +24,6 @@ export function registerCloudBackupStatusRoutes({ repository: CLOUD_SNAPSHOT_REPOSITORY, snapshot: '_all', ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. - // @ts-expect-error @elastic/elasticsearch "desc" is a new param order: 'desc', sort: 'start_time', size: 1, diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts index ba973a7aa8a616..859954364f9d3a 100644 --- a/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts @@ -23,7 +23,7 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { }); step('Open monitor status flyout', async () => { - await page.click(byTestId('createFirstAlertButton')); + await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"')); await waitForLoadingToFinish({ page }); @@ -54,7 +54,7 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { }); step('Open tls alert flyout', async () => { - await page.click(byTestId('createFirstAlertButton')); + await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); await waitForLoadingToFinish({ page }); diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts index 024e8e53c3b2a1..d8b53f9f3c89ff 100644 --- a/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts @@ -23,7 +23,7 @@ journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { }); step('Open tls alert flyout', async () => { - await page.click(byTestId('createFirstAlertButton')); + await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); await waitForLoadingToFinish({ page }); diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts index c44dbc187bd53d..565dcc56cc9834 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_details/monitor_alerts.journey.ts @@ -66,7 +66,7 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = }); step('close anomaly detection flyout', async () => { - await page.click(byTestId('cancelSaveAlertButton')); + await page.click(byTestId('cancelSaveRuleButton')); }); step('open anomaly detection alert', async () => { @@ -80,7 +80,7 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = }); step('save anomaly detection alert', async () => { - await page.click(byTestId('saveAlertButton')); + await page.click(byTestId('saveRuleButton')); await page.click(byTestId('confirmModalConfirmButton')); await page.waitForSelector(`text=Created rule "${alertId}"`); }); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx index efadc26a383c01..7cce2c061fa820 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_details.tsx @@ -99,7 +99,7 @@ export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; ki }, async updateAlert({ id, threshold }: AlertType) { - await this.fillByTestSubj('alertNameInput', id); + await this.fillByTestSubj('ruleNameInput', id); await this.selectAlertThreshold(threshold); }, diff --git a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx index fbad21cbfaed5c..a12589cc749713 100644 --- a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -7,14 +7,15 @@ import React, { useMemo } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { +import type { Rule, TriggersAndActionsUIPublicPluginStart, } from '../../../../../triggers_actions_ui/public'; +import { UptimeAlertTypeParams } from '../../../state/alerts/alerts'; interface Props { alertFlyoutVisible: boolean; - initialAlert: Rule; + initialAlert: Rule; setAlertFlyoutVisibility: React.Dispatch>; } @@ -32,7 +33,7 @@ export const UptimeEditAlertFlyoutComponent = ({ const EditAlertFlyout = useMemo( () => triggersActionsUi.getEditAlertFlyout({ - initialAlert, + initialRule: initialAlert, onClose: () => { setAlertFlyoutVisibility(false); }, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx index e79150a4eb79e0..ec58ac7ee5010c 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx @@ -15,7 +15,7 @@ describe('', () => { const onUpdate = jest.fn(); it('navigates to edit monitor flow on edit pencil', () => { - render(); + render(); expect(screen.getByLabelText('Edit monitor')).toHaveAttribute( 'href', diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx index 5fa29b7cd7c56c..9d84263f3701e1 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx @@ -13,11 +13,12 @@ import { DeleteMonitor } from './delete_monitor'; interface Props { id: string; + name: string; isDisabled?: boolean; onUpdate: () => void; } -export const Actions = ({ id, onUpdate, isDisabled }: Props) => { +export const Actions = ({ id, name, onUpdate, isDisabled }: Props) => { const { basePath } = useContext(UptimeSettingsContext); return ( @@ -32,7 +33,7 @@ export const Actions = ({ id, onUpdate, isDisabled }: Props) => { /> - + ); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx index a5c712b6e04569..2e69196c86cff4 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx @@ -26,7 +26,7 @@ describe('', () => { useFetcher.mockImplementation(originalUseFetcher); const deleteMonitor = jest.spyOn(fetchers, 'deleteMonitor'); const id = 'test-id'; - render(); + render(); expect(deleteMonitor).not.toBeCalled(); @@ -39,7 +39,8 @@ describe('', () => { it('calls set refresh when deletion is successful', () => { const id = 'test-id'; - render(); + const name = 'sample monitor'; + render(); userEvent.click(screen.getByLabelText('Delete monitor')); @@ -53,7 +54,7 @@ describe('', () => { status: FETCH_STATUS.LOADING, refetch: () => {}, }); - render(); + render(); expect(screen.getByLabelText('Deleting monitor...')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx index e0b706241b79aa..88bace104adacc 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.tsx @@ -16,10 +16,12 @@ import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/publ export const DeleteMonitor = ({ id, + name, onUpdate, isDisabled, }: { id: string; + name: string; isDisabled?: boolean; onUpdate: () => void; }) => { @@ -70,7 +72,7 @@ export const DeleteMonitor = ({ const destroyModal = ( setIsDeleteModalVisible(false)} onConfirm={onConfirmDelete} cancelButtonText={NO_LABEL} @@ -103,16 +105,17 @@ export const DeleteMonitor = ({ const DELETE_DESCRIPTION_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.confirmDescriptionLabel', { - defaultMessage: 'Are you sure you want to do delete the monitor?', + defaultMessage: + 'This action will delete the monitor but keep any data collected. This action cannot be undone.', } ); const YES_LABEL = i18n.translate('xpack.uptime.monitorManagement.yesLabel', { - defaultMessage: 'Yes', + defaultMessage: 'Delete', }); const NO_LABEL = i18n.translate('xpack.uptime.monitorManagement.noLabel', { - defaultMessage: 'No', + defaultMessage: 'Cancel', }); const DELETE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.deleteMonitorLabel', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx index a222070b5d9d14..5d18fdcaca6fea 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx @@ -179,11 +179,17 @@ export const MonitorManagementList = ({ }, { align: 'left' as const, - field: 'id', name: i18n.translate('xpack.uptime.monitorManagement.monitorList.actions', { defaultMessage: 'Actions', }), - render: (id: string) => , + render: (fields: SyntheticsMonitorWithId) => ( + + ), }, ] as Array>; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 459f73a78ad8bc..cf3a9d0800aead 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -34,7 +34,7 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ triggersActionsUi.getAddAlertFlyout({ consumer: 'uptime', onClose: onCloseAlertFlyout, - alertTypeId, + ruleTypeId: alertTypeId, canChangeTrigger: !alertTypeId, }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx index 3d51051d28fe53..e35234aab54b09 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx @@ -13,16 +13,16 @@ import type { CoreTheme } from 'kibana/public'; import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { RedirectAppLinks, toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { ActionConnector } from '../../state/alerts/alerts'; -import { Alert } from '../../../../alerting/common'; import { kibanaService } from '../../state/kibana_service'; import { getUrlForAlert } from './common'; +import type { Rule } from '../../../../triggers_actions_ui/public'; export const simpleAlertEnabled = ( defaultActions: ActionConnector[], theme$: Observable, - alert: Alert + rule: Rule ) => { - const alertUrl = getUrlForAlert(alert.id, kibanaService.core.http.basePath.get()); + const alertUrl = getUrlForAlert(rule.id, kibanaService.core.http.basePath.get()); return { title: i18n.translate('xpack.uptime.overview.alerts.enabled.success', { diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts index 70c92d825adde6..13072c1b884e1b 100644 --- a/x-pack/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -7,7 +7,7 @@ import { Action } from 'redux-actions'; import { IHttpFetchError } from 'src/core/public'; -import { Alert } from '../../../../alerting/common'; +import type { Rule } from '../../../../triggers_actions_ui/public'; import { UptimeAlertTypeParams } from '../alerts/alerts'; export interface AsyncAction { @@ -63,5 +63,5 @@ export interface AlertsResult { page: number; perPage: number; total: number; - data: Array>; + data: Array>; } diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index c33bb7bde01b81..757ee2acec0bda 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -21,8 +21,10 @@ import { fetchMonitorAlertRecords, NewAlertParams, } from '../api/alerts'; -import { ActionConnector as RawActionConnector } from '../../../../triggers_actions_ui/public'; -import { Alert } from '../../../../alerting/common'; +import type { + ActionConnector as RawActionConnector, + Rule, +} from '../../../../triggers_actions_ui/public'; import { kibanaService } from '../kibana_service'; import { monitorIdSelector } from '../selectors'; import { AlertsResult, MonitorIdParam } from '../actions/types'; @@ -37,15 +39,14 @@ export type UptimeAlertTypeParams = Record; export const createAlertAction = createAsyncAction< NewAlertParams, - Alert | null + Rule | null >('CREATE ALERT'); export const getConnectorsAction = createAsyncAction<{}, ActionConnector[]>('GET CONNECTORS'); export const getMonitorAlertsAction = createAsyncAction<{}, AlertsResult | null>('GET ALERTS'); -export const getAnomalyAlertAction = createAsyncAction< - MonitorIdParam, - Alert ->('GET EXISTING ALERTS'); +export const getAnomalyAlertAction = createAsyncAction>( + 'GET EXISTING ALERTS' +); export const deleteAlertAction = createAsyncAction<{ alertId: string }, string | null>( 'DELETE ALERTS' ); @@ -55,9 +56,9 @@ export const deleteAnomalyAlertAction = createAsyncAction<{ alertId: string }, a export interface AlertState { connectors: AsyncInitState; - newAlert: AsyncInitState>; + newAlert: AsyncInitState>; alerts: AsyncInitState; - anomalyAlert: AsyncInitState>; + anomalyAlert: AsyncInitState>; alertDeletion: AsyncInitState; anomalyAlertDeletion: AsyncInitState; } @@ -147,7 +148,7 @@ export function* fetchAlertsEffect() { ); yield takeLatest(createAlertAction.get, function* (action: Action): Generator { try { - const response = (yield call(createAlert, action.payload)) as Alert; + const response = (yield call(createAlert, action.payload)) as Rule; yield put(createAlertAction.success(response)); kibanaService.core.notifications.toasts.addSuccess( diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts index 0e29300f02a651..93a19cedc6001b 100644 --- a/x-pack/plugins/uptime/public/state/api/alert_actions.ts +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { NewAlertParams } from './alerts'; -import { AlertAction } from '../../../../triggers_actions_ui/public'; +import { RuleAction as RuleActionOrig } from '../../../../triggers_actions_ui/public'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { MonitorStatusTranslations } from '../../../common/translations'; import { @@ -36,7 +36,7 @@ export const EMAIL_ACTION_ID: ActionTypeId = '.email'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; -export type RuleAction = Omit; +export type RuleAction = Omit; const getRecoveryMessage = (selectedMonitor: Ping) => { return i18n.translate('xpack.uptime.alerts.monitorStatus.recoveryMessage', { diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts index 7ddfbb872fb95a..d19b9688b21d00 100644 --- a/x-pack/plugins/uptime/public/state/api/alerts.ts +++ b/x-pack/plugins/uptime/public/state/api/alerts.ts @@ -10,9 +10,9 @@ import { apiService } from './utils'; import { ActionConnector } from '../alerts/alerts'; import { AlertsResult, MonitorIdParam } from '../actions/types'; -import { ActionType, AsApiContract } from '../../../../triggers_actions_ui/public'; +import type { ActionType, AsApiContract, Rule } from '../../../../triggers_actions_ui/public'; import { API_URLS } from '../../../common/constants'; -import { Alert, AlertTypeParams } from '../../../../alerting/common'; +import { AlertTypeParams } from '../../../../alerting/common'; import { AtomicStatusCheckParams } from '../../../common/runtime_types/alerts'; import { populateAlertActions, RuleAction } from './alert_actions'; @@ -49,7 +49,7 @@ export interface NewAlertParams extends AlertTypeParams { } type NewMonitorStatusAlert = Omit< - Alert, + Rule, | 'id' | 'createdBy' | 'updatedBy' @@ -60,12 +60,12 @@ type NewMonitorStatusAlert = Omit< | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' - | 'alertTypeId' + | 'ruleTypeId' | 'notifyWhen' | 'actions' > & { - rule_type_id: Alert['alertTypeId']; - notify_when: Alert['notifyWhen']; + rule_type_id: Rule['ruleTypeId']; + notify_when: Rule['notifyWhen']; actions: RuleAction[]; }; @@ -74,7 +74,7 @@ export const createAlert = async ({ monitorId, selectedMonitor, defaultEmail, -}: NewAlertParams): Promise => { +}: NewAlertParams): Promise => { const actions: RuleAction[] = populateAlertActions({ defaultActions, selectedMonitor, @@ -122,7 +122,7 @@ export const fetchMonitorAlertRecords = async (): Promise => { export const fetchAlertRecords = async ({ monitorId, -}: MonitorIdParam): Promise> => { +}: MonitorIdParam): Promise> => { const data = { page: 1, per_page: 500, @@ -131,11 +131,16 @@ export const fetchAlertRecords = async ({ sort_field: 'name.keyword', sort_order: 'asc', }; - const alerts = await apiService.get<{ data: Array> }>( - API_URLS.RULES_FIND, - data - ); - return alerts.data.find((alert) => alert.params.monitorId === monitorId) as Alert; + const rawRules = await apiService.get<{ + data: Array & { rule_type_id: string }>; + }>(API_URLS.RULES_FIND, data); + const monitorRule = rawRules.data.find( + (rule) => rule.params.monitorId === monitorId + ) as Rule & { rule_type_id: string }; + return { + ...monitorRule, + ruleTypeId: monitorRule.rule_type_id, + }; }; export const disableAlertById = async ({ alertId }: { alertId: string }) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts b/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts new file mode 100644 index 00000000000000..48fa6e45f19a89 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/action_variables.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 MESSAGE = 'message'; +export const MONITOR_WITH_GEO = 'downMonitorsWithGeo'; +export const ALERT_REASON_MSG = 'reason'; + +export const ACTION_VARIABLES = { + [MESSAGE]: { + name: MESSAGE, + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', + { + defaultMessage: 'A generated message summarizing the currently down monitors', + } + ), + }, + [MONITOR_WITH_GEO]: { + name: MONITOR_WITH_GEO, + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', + { + defaultMessage: + 'A generated summary that shows some or all of the monitors detected as "down" by the alert', + } + ), + }, + [ALERT_REASON_MSG]: { + name: ALERT_REASON_MSG, + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.alertReasonMessage.description', + { + defaultMessage: 'A concise description of the reason for the alert', + } + ), + }, +}; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index ff6d5d42de5102..208f19354a0f3a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -16,6 +16,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; import { Ping } from '../../../common/runtime_types/ping'; +import { ALERT_REASON_MSG } from './action_variables'; interface MockAnomaly { severity: AnomaliesTableRecord['severity']; @@ -157,6 +158,7 @@ describe('duration anomaly alert', () => { ); const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); + const reasonMessages: string[] = []; mockAnomaliesResult.anomalies.forEach((anomaly, index) => { const slowestResponse = Math.round(anomaly.actualSort / 1000); const typicalResponse = Math.round(anomaly.typicalSort / 1000); @@ -180,6 +182,7 @@ Response times as high as ${slowestResponse} ms have been detected from location }, id: `${DURATION_ANOMALY.id}${index}`, }); + expect(alertInstanceMock.replaceState).toBeCalledWith({ firstCheckedAt: 'date', firstTriggeredAt: undefined, @@ -198,9 +201,35 @@ Response times as high as ${slowestResponse} ms have been detected from location slowestAnomalyResponse: `${slowestResponse} ms`, bucketSpan: anomaly.source.bucket_span, }); + const reasonMsg = `Abnormal (${getSeverityType( + anomaly.severity + )} level) response time detected on uptime-monitor with url ${ + mockPing.url?.full + } at date. Anomaly severity score is ${anomaly.severity}. + Response times as high as ${slowestResponse} ms have been detected from location ${ + anomaly.entityValue + }. Expected response time is ${typicalResponse} ms.`; + + reasonMessages.push(reasonMsg); }); expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.scheduleActions).toBeCalledWith(DURATION_ANOMALY.id); + + expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "xpack.uptime.alerts.actionGroups.durationAnomaly", + Object { + "${ALERT_REASON_MSG}": "${reasonMessages[0]}", + }, + ] + `); + expect(alertInstanceMock.scheduleActions.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "xpack.uptime.alerts.actionGroups.durationAnomaly", + Object { + "${ALERT_REASON_MSG}": "${reasonMessages[1]}", + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index b2f3ca0ad6d353..1dcb91b9e5270f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -26,6 +26,7 @@ import { getMLJobId } from '../../../common/lib'; import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations'; import { createUptimeESClient } from '../lib'; +import { ALERT_REASON_MSG, ACTION_VARIABLES } from './action_variables'; export type ActionGroupIds = ActionGroupIdsOf; @@ -92,7 +93,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }, ], actionVariables: { - context: [], + context: [ACTION_VARIABLES[ALERT_REASON_MSG]], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, isExportable: true, @@ -122,6 +123,10 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory anomalies.forEach((anomaly, index) => { const summary = getAnomalySummary(anomaly, monitorInfo); + const alertReasonMessage = generateAlertMessage( + CommonDurationAnomalyTranslations.defaultActionMessage, + summary + ); const alertInstance = alertWithLifecycle({ id: DURATION_ANOMALY.id + index, @@ -133,17 +138,16 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory 'anomaly.bucket_span.minutes': summary.bucketSpan, [ALERT_EVALUATION_VALUE]: anomaly.actualSort, [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, - [ALERT_REASON]: generateAlertMessage( - CommonDurationAnomalyTranslations.defaultActionMessage, - summary - ), + [ALERT_REASON]: alertReasonMessage, }, }); alertInstance.replaceState({ ...updateState(state, false), ...summary, }); - alertInstance.scheduleActions(DURATION_ANOMALY.id); + alertInstance.scheduleActions(DURATION_ANOMALY.id, { + [ALERT_REASON_MSG]: alertReasonMessage, + }); }); } diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index cea34b6daad96a..d2e4a8dbc044ec 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -241,6 +241,9 @@ describe('status check alert', () => { expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "First from harrisburg failed 234 times in the last 15 mins. Alert when > 5.", + }, ] `); }); @@ -308,6 +311,9 @@ describe('status check alert', () => { expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "First from harrisburg failed 234 times in the last 15m. Alert when > 5.", + }, ] `); }); @@ -776,15 +782,27 @@ describe('status check alert', () => { Array [ Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "Foo from harrisburg 35 days availability is 99.28%. Alert when < 99.34%.", + }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "Foo from fairbanks 35 days availability is 98.03%. Alert when < 99.34%.", + }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "Unreliable from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "reason": "no-name from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + }, ], ] `); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index aa803a45fcdce5..fe93928cb7e02e 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -36,6 +36,7 @@ import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/g import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../../observability/common'; +import { ALERT_REASON_MSG, MESSAGE, MONITOR_WITH_GEO, ACTION_VARIABLES } from './action_variables'; export type ActionGroupIds = ActionGroupIdsOf; /** @@ -268,25 +269,9 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ], actionVariables: { context: [ - { - name: 'message', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', - { - defaultMessage: 'A generated message summarizing the currently down monitors', - } - ), - }, - { - name: 'downMonitorsWithGeo', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', - { - defaultMessage: - 'A generated summary that shows some or all of the monitors detected as "down" by the alert', - } - ), - }, + ACTION_VARIABLES[MESSAGE], + ACTION_VARIABLES[MONITOR_WITH_GEO], + ACTION_VARIABLES[ALERT_REASON_MSG], ], state: [...commonMonitorStateI18, ...commonStateTranslations], }, @@ -375,7 +360,9 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ...updateState(state, true), }); - alert.scheduleActions(MONITOR_STATUS.id); + alert.scheduleActions(MONITOR_STATUS.id, { + [ALERT_REASON_MSG]: monitorSummary.reason, + }); } return updateState(state, downMonitorsByLocation.length > 0); } @@ -432,7 +419,9 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( statusMessage, }); - alert.scheduleActions(MONITOR_STATUS.id); + alert.scheduleActions(MONITOR_STATUS.id, { + [ALERT_REASON_MSG]: monitorSummary.reason, + }); }); return updateState(state, downMonitorsByLocation.length > 0); diff --git a/x-pack/test/fleet_api_integration/apis/outputs/index.js b/x-pack/test/fleet_api_integration/apis/outputs/index.js index b799413638d474..b01c253defe76e 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/index.js +++ b/x-pack/test/fleet_api_integration/apis/outputs/index.js @@ -8,5 +8,6 @@ export default function loadTests({ loadTestFile }) { describe('Output Endpoints', () => { loadTestFile(require.resolve('./crud')); + loadTestFile(require.resolve('./logstash_api_keys')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/outputs/logstash_api_keys.ts b/x-pack/test/fleet_api_integration/apis/outputs/logstash_api_keys.ts new file mode 100644 index 00000000000000..316b4d09bda1b2 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/outputs/logstash_api_keys.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { getEsClientForAPIKey } from '../agents/services'; +import { testUsers } from '../test_users'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('fleet_output_logstash_api_keys', async function () { + describe('POST /logstash_api_keys', () => { + it('should allow to create an api key with the right permissions', async () => { + const { body: apiKeyRes } = await supertest + .post(`/api/fleet/logstash_api_keys`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(apiKeyRes).to.have.keys('api_key'); + + const { body: privileges } = await getEsClientForAPIKey( + providerContext, + Buffer.from(apiKeyRes.api_key).toString('base64') + ).security.hasPrivileges( + { + body: { + cluster: ['monitor'], + index: [ + { + names: [ + 'logs-*-*', + 'metrics-*-*', + 'traces-*-*', + 'synthetics-*-*', + '.logs-endpoint.diagnostic.collection-*', + '.logs-endpoint.action.responses-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }, + { meta: true } + ); + + expect(privileges.has_all_requested).to.be(true); + }); + }); + + it('should return a 400 with a user without the correct ES permissions', async () => { + await supertestWithoutAuth + .post(`/api/fleet/logstash_api_keys`) + .auth(testUsers.fleet_all_int_all.username, testUsers.fleet_all_int_all.password) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts b/x-pack/test/functional/apps/data_views/feature_controls/index.ts similarity index 78% rename from x-pack/test/functional/apps/index_patterns/feature_controls/index.ts rename to x-pack/test/functional/apps/data_views/feature_controls/index.ts index 8eca3e20cd0d11..17a8da9af0d6bb 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts +++ b/x-pack/test/functional/apps/data_views/feature_controls/index.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { this.tags('skipFirefox'); - loadTestFile(require.resolve('./index_patterns_security')); - loadTestFile(require.resolve('./index_patterns_spaces')); + loadTestFile(require.resolve('./security')); + loadTestFile(require.resolve('./spaces')); }); } diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/data_views/feature_controls/security.ts similarity index 96% rename from x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts rename to x-pack/test/functional/apps/data_views/feature_controls/security.ts index 20dd08fab14962..96682302b57136 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/data_views/feature_controls/security.ts @@ -27,7 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); }); - describe('global index_patterns all privileges', () => { + describe('global data views all privileges', () => { before(async () => { await security.role.create('global_index_patterns_all_role', { elasticsearch: { @@ -87,7 +87,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('global index_patterns read-only privileges', () => { + describe('global data views read-only privileges', () => { before(async () => { await security.role.create('global_index_patterns_read_role', { elasticsearch: { @@ -142,7 +142,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('no index_patterns privileges', () => { + describe('no data views privileges', () => { before(async () => { await security.role.create('no_index_patterns_privileges_role', { elasticsearch: { @@ -183,7 +183,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Discover']); }); - it(`doesn't show Index Patterns in management side-nav`, async () => { + it(`doesn't show Data Views in management side-nav`, async () => { await PageObjects.common.navigateToActualUrl('management', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/data_views/feature_controls/spaces.ts similarity index 96% rename from x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts rename to x-pack/test/functional/apps/data_views/feature_controls/spaces.ts index 10a25da4ef0fa0..9b6105b21eecff 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/data_views/feature_controls/spaces.ts @@ -47,14 +47,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.contain('Stack Management'); }); - it(`index pattern listing shows create button`, async () => { + it(`data views listing shows create button`, async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await testSubjects.existOrFail('createIndexPatternButton'); }); }); - describe('space with Index Patterns disabled', () => { + describe('space with Data Views disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/index_patterns/index.ts b/x-pack/test/functional/apps/data_views/index.ts similarity index 82% rename from x-pack/test/functional/apps/index_patterns/index.ts rename to x-pack/test/functional/apps/data_views/index.ts index bf9acf29cc2c11..3b3f7b36081133 100644 --- a/x-pack/test/functional/apps/index_patterns/index.ts +++ b/x-pack/test/functional/apps/data_views/index.ts @@ -8,8 +8,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function advancedSettingsApp({ loadTestFile }: FtrProviderContext) { - describe('Index Patterns', function indexPatternsTestSuite() { + describe('Data Views', function indexPatternsTestSuite() { this.tags('ciGroup2'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./spaces')); }); } diff --git a/x-pack/test/functional/apps/data_views/spaces/index.ts b/x-pack/test/functional/apps/data_views/spaces/index.ts new file mode 100644 index 00000000000000..0d5bccd156b0e1 --- /dev/null +++ b/x-pack/test/functional/apps/data_views/spaces/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'common', + 'spaceSelector', + 'home', + 'header', + 'security', + 'settings', + ]); + const spacesService = getService('spaces'); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + + describe('spaces', function () { + this.tags('skipFirefox'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + }); + + it('it can add a space', async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.createIndexPattern('log*'); + + await PageObjects.settings.clickKibanaIndexPatterns(); + + // click manage spaces on first entry + await (await testSubjects.findAll('manageSpacesButton', 10000))[0].click(); + + // select custom space + await testSubjects.click('sts-space-selector-row-custom_space'); + await testSubjects.click('sts-save-button'); + + // verify custom space has been added to list + await testSubjects.existOrFail('space-avatar-custom_space'); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts index 4edf87ab8d1fbe..e5b2fb30d4e456 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts @@ -16,7 +16,6 @@ export default function ({ getService }: FtrProviderContext) { const supportedTestSuites = [ { suiteTitle: 'supported job with aggregation field', - // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_supported_aggs_${ts}`, job_type: 'anomaly_detector', @@ -103,7 +102,6 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteTitle: 'supported job with scripted field', - // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_supported_script_${ts}`, job_type: 'anomaly_detector', @@ -178,7 +176,6 @@ export default function ({ getService }: FtrProviderContext) { const unsupportedTestSuites = [ { suiteTitle: 'unsupported job with bucket_script aggregation field', - // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_unsupported_aggs_${ts}`, job_type: 'anomaly_detector', @@ -283,7 +280,6 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteTitle: 'unsupported job with partition by of a scripted field', - // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_unsupported_script_${ts}`, job_type: 'anomaly_detector', diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts index a31b9faa169f98..c3d0ee718cecd7 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -11,7 +11,6 @@ import type { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/ const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ { - // @ts-expect-error not full interface job: { job_id: 'fq_single_1_smv', groups: ['farequote', 'automated', 'single-metric'], @@ -64,7 +63,6 @@ const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ }, }, { - // @ts-expect-error not full interface job: { job_id: 'fq_single_2_smv', groups: ['farequote', 'automated', 'single-metric'], @@ -117,7 +115,6 @@ const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ }, }, { - // @ts-expect-error not full interface job: { job_id: 'fq_single_3_smv', groups: ['farequote', 'automated', 'single-metric'], diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index b2893ace7b20aa..c29135e089bfa2 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -12,9 +12,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); const es = getService('es'); + const security = getService('security'); describe('Home page', function () { before(async () => { + await security.testUser.setRoles(['snapshot_restore_user'], false); await pageObjects.common.navigateToApp('snapshotRestore'); }); @@ -46,9 +48,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('cleanup repository', async () => { await pageObjects.snapshotRestore.viewRepositoryDetails('my-repository'); - await pageObjects.common.sleep(25000); const cleanupResponse = await pageObjects.snapshotRestore.performRepositoryCleanup(); - await pageObjects.common.sleep(25000); expect(cleanupResponse).to.contain('results'); expect(cleanupResponse).to.contain('deleted_bytes'); expect(cleanupResponse).to.contain('deleted_blobs'); @@ -57,6 +57,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await es.snapshot.deleteRepository({ name: 'my-repository', }); + await security.testUser.restoreDefaults(); }); }); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 9e70c8d4ed7fd1..66b32dac8e4b7b 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -53,7 +53,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/dev_tools'), resolve(__dirname, './apps/apm'), resolve(__dirname, './apps/api_keys'), - resolve(__dirname, './apps/index_patterns'), + resolve(__dirname, './apps/data_views'), resolve(__dirname, './apps/index_management'), resolve(__dirname, './apps/index_lifecycle_management'), resolve(__dirname, './apps/ingest_pipelines'), @@ -551,6 +551,25 @@ export default async function ({ readConfigFile }) { }, ], }, + // https://www.elastic.co/guide/en/elasticsearch/reference/master/snapshots-register-repository.html#snapshot-repo-prereqs + snapshot_restore_user: { + elasticsearch: { + cluster: [ + 'monitor', + 'manage_slm', + 'cluster:admin/snapshot', + 'cluster:admin/repository', + ], + }, + kibana: [ + { + feature: { + advancedSettings: ['read'], + }, + spaces: ['*'], + }, + ], + }, ingest_pipelines_user: { elasticsearch: { diff --git a/x-pack/test/functional/page_objects/snapshot_restore_page.ts b/x-pack/test/functional/page_objects/snapshot_restore_page.ts index 216b2bfff9d7ed..e1fc50ed86fa2d 100644 --- a/x-pack/test/functional/page_objects/snapshot_restore_page.ts +++ b/x-pack/test/functional/page_objects/snapshot_restore_page.ts @@ -49,12 +49,15 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) const repoToView = repos.filter((r) => (r.repoName = name))[0]; await repoToView.repoLink.click(); } - await retry.waitForWithTimeout(`Repo title should be ${name}`, 10000, async () => { + await retry.waitForWithTimeout(`Repo title should be ${name}`, 25000, async () => { return (await testSubjects.getVisibleText('title')) === name; }); }, async performRepositoryCleanup() { await testSubjects.click('cleanupRepositoryButton'); + await retry.waitForWithTimeout(`wait for code block to be visible`, 25000, async () => { + return await testSubjects.isDisplayed('cleanupCodeBlock'); + }); return await testSubjects.getVisibleText('cleanupCodeBlock'); }, }; diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index da34c97ff127ff..4341fb12471199 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -55,7 +55,7 @@ export function MachineLearningNavigationProvider({ async navigateToAlertsAndAction() { await PageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); }, async assertTabsExist(tabTypeSubject: string, areaSubjects: string[]) { diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index 9275b9049ab09d..76bea59cc4207d 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -21,7 +21,7 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { await testSubjects.click('xpack.uptime.toggleTlsAlertFlyout'); } // ensure the flyout has opened - await testSubjects.exists('alertNameInput'); + await testSubjects.exists('ruleNameInput'); }, async openMonitorStatusAlertType(alertType: string) { await testSubjects.click(`xpack.uptime.alerts.${alertType}-SelectOption`); @@ -34,7 +34,7 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { } }, async setAlertName(name: string) { - await testSubjects.setValue('alertNameInput', name); + await testSubjects.setValue('ruleNameInput', name); }, async setAlertInterval(value: string) { await testSubjects.setValue('intervalInput', value); @@ -104,11 +104,11 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { await testSubjects.click('uptimeAlertAddFilter.monitor.type'); await testSubjects.click('uptimeCreateStatusAlert.filter_scheme'); }, - async clickSaveAlertButton() { - await testSubjects.click('saveAlertButton'); + async clickSaveRuleButton() { + await testSubjects.click('saveRuleButton'); }, async clickSaveAlertsConfirmButton() { - await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton', 20000); + await testSubjects.click('confirmRuleSaveModal > confirmModalConfirmButton', 20000); }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 95ff24fc8beef1..1db679d6f1286a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -41,7 +41,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function defineEsQueryAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click(`.es-query-SelectOption`); await testSubjects.click('selectIndexExpression'); const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); @@ -57,13 +57,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await testSubjects.click('closePopover'); // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('alertNameInput'); + const nameInput = await testSubjects.find('ruleNameInput'); await nameInput.click(); } async function defineIndexThresholdAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click(`.index-threshold-SelectOption`); await testSubjects.click('selectIndexExpression'); const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); @@ -79,7 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await testSubjects.click('closePopover'); // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('alertNameInput'); + const nameInput = await testSubjects.find('ruleNameInput'); await nameInput.click(); await testSubjects.click('whenExpression'); @@ -101,7 +101,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click('test.always-firing-SelectOption'); } @@ -157,7 +157,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'test message {{alert.actionGroup}} some additional text {{rule.id}}' ); - await testSubjects.click('saveAlertButton'); + await testSubjects.click('saveRuleButton'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Created rule "${alertName}"`); await pageObjects.triggersActionsUI.searchAlerts(alertName); @@ -207,7 +207,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('addNewActionConnectorActionGroup-1'); await testSubjects.click('addNewActionConnectorActionGroup-1-option-other'); - await testSubjects.click('saveAlertButton'); + await testSubjects.click('saveRuleButton'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Created rule "${alertName}"`); await pageObjects.triggersActionsUI.searchAlerts(alertName); @@ -228,16 +228,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alertName = generateUniqueKey(); await defineAlwaysFiringAlert(alertName); - await testSubjects.click('saveAlertButton'); - await testSubjects.existOrFail('confirmAlertSaveModal'); - await testSubjects.click('confirmAlertSaveModal > confirmModalCancelButton'); - await testSubjects.missingOrFail('confirmAlertSaveModal'); - await find.existsByCssSelector('[data-test-subj="saveAlertButton"]:not(disabled)'); + await testSubjects.click('saveRuleButton'); + await testSubjects.existOrFail('confirmRuleSaveModal'); + await testSubjects.click('confirmRuleSaveModal > confirmModalCancelButton'); + await testSubjects.missingOrFail('confirmRuleSaveModal'); + await find.existsByCssSelector('[data-test-subj="saveRuleButton"]:not(disabled)'); - await testSubjects.click('saveAlertButton'); - await testSubjects.existOrFail('confirmAlertSaveModal'); - await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); - await testSubjects.missingOrFail('confirmAlertSaveModal'); + await testSubjects.click('saveRuleButton'); + await testSubjects.existOrFail('confirmRuleSaveModal'); + await testSubjects.click('confirmRuleSaveModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmRuleSaveModal'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Created rule "${alertName}"`); @@ -258,15 +258,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show discard confirmation before closing flyout without saving', async () => { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.click('cancelSaveAlertButton'); - await testSubjects.missingOrFail('confirmAlertCloseModal'); + await testSubjects.click('cancelSaveRuleButton'); + await testSubjects.missingOrFail('confirmRuleCloseModal'); await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('intervalInput', '10'); - await testSubjects.click('cancelSaveAlertButton'); - await testSubjects.existOrFail('confirmAlertCloseModal'); - await testSubjects.click('confirmAlertCloseModal > confirmModalCancelButton'); - await testSubjects.missingOrFail('confirmAlertCloseModal'); + await testSubjects.click('cancelSaveRuleButton'); + await testSubjects.existOrFail('confirmRuleCloseModal'); + await testSubjects.click('confirmRuleCloseModal > confirmModalCancelButton'); + await testSubjects.missingOrFail('confirmRuleCloseModal'); }); it('should successfully test valid es_query alert', async () => { @@ -281,9 +281,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.existOrFail('testQuerySuccess'); await testSubjects.missingOrFail('testQueryError'); - await testSubjects.click('cancelSaveAlertButton'); - await testSubjects.existOrFail('confirmAlertCloseModal'); - await testSubjects.click('confirmAlertCloseModal > confirmModalConfirmButton'); + await testSubjects.click('cancelSaveRuleButton'); + await testSubjects.existOrFail('confirmRuleCloseModal'); + await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton'); }); it('should show error when es_query is invalid', async () => { @@ -301,19 +301,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show all rule types on click euiFormControlLayoutClearButton', async () => { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', 'alertName'); - const ruleTypeSearchBox = await find.byCssSelector('[data-test-subj="alertSearchField"]'); + await testSubjects.setValue('ruleNameInput', 'alertName'); + const ruleTypeSearchBox = await find.byCssSelector('[data-test-subj="ruleSearchField"]'); await ruleTypeSearchBox.type('notexisting rule type'); await ruleTypeSearchBox.pressKeys(browser.keys.ENTER); - const ruleTypes = await find.allByCssSelector('.triggersActionsUI__alertTypeNodeHeading'); + const ruleTypes = await find.allByCssSelector('.triggersActionsUI__ruleTypeNodeHeading'); expect(ruleTypes).to.have.length(0); const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); await searchClearButton.click(); const ruleTypesClearFilter = await find.allByCssSelector( - '.triggersActionsUI__alertTypeNodeHeading' + '.triggersActionsUI__ruleTypeNodeHeading' ); expect(ruleTypesClearFilter.length).to.above(0); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 2b45a127901079..0f6e99ccf27f38 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -108,7 +108,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); await searchClearButton.click(); await find.byCssSelector( - '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' + '.euiBasicTable[data-test-subj="rulesList"]:not(.euiBasicTable-loading)' ); const searchResultsAfterClear = await pageObjects.triggersActionsUI.getAlertsList(); expect(searchResultsAfterClear.length).to.equal(2); @@ -257,7 +257,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await testSubjects.click('deleteAlert'); + await testSubjects.click('deleteRule'); await testSubjects.existOrFail('deleteIdsConfirmation'); await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton'); await testSubjects.missingOrFail('deleteIdsConfirmation'); @@ -365,7 +365,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await createAlert({ supertest, objectRemover }); await refreshAlertsList(); - await testSubjects.existOrFail('alertsTable-P50ColumnName'); + await testSubjects.existOrFail('rulesTable-P50ColumnName'); await testSubjects.existOrFail('P50Percentile'); await retry.try(async () => { @@ -385,7 +385,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await searchClearButton.click(); await testSubjects.missingOrFail('percentileSelectablePopover-selectable'); - await testSubjects.existOrFail('alertsTable-P95ColumnName'); + await testSubjects.existOrFail('rulesTable-P95ColumnName'); await testSubjects.existOrFail('P95Percentile'); }); }); @@ -427,8 +427,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); expect(refreshResults.map((item: any) => item.status).sort()).to.eql(['Error', 'Ok']); }); - await testSubjects.click('alertStatusFilterButton'); - await testSubjects.click('alertStatuserrorFilerOption'); // select Error status filter + await testSubjects.click('ruleStatusFilterButton'); + await testSubjects.click('ruleStatuserrorFilerOption'); // select Error status filter await retry.try(async () => { const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); @@ -453,7 +453,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); const alertsErrorBannerWhenNoErrors = await find.allByCssSelector( - '[data-test-subj="alertsErrorBanner"]' + '[data-test-subj="rulesErrorBanner"]' ); expect(alertsErrorBannerWhenNoErrors).to.have.length(0); @@ -461,7 +461,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { await refreshAlertsList(); const alertsErrorBannerExistErrors = await find.allByCssSelector( - '[data-test-subj="alertsErrorBanner"]' + '[data-test-subj="rulesErrorBanner"]' ); expect(alertsErrorBannerExistErrors).to.have.length(1); expect( @@ -472,21 +472,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await refreshAlertsList(); - expect(await testSubjects.getVisibleText('totalAlertsCount')).to.be('Showing: 2 of 2 rules.'); - expect(await testSubjects.getVisibleText('totalActiveAlertsCount')).to.be('Active: 0'); - expect(await testSubjects.getVisibleText('totalOkAlertsCount')).to.be('Ok: 1'); - expect(await testSubjects.getVisibleText('totalErrorAlertsCount')).to.be('Error: 1'); - expect(await testSubjects.getVisibleText('totalPendingAlertsCount')).to.be('Pending: 0'); - expect(await testSubjects.getVisibleText('totalUnknownAlertsCount')).to.be('Unknown: 0'); + expect(await testSubjects.getVisibleText('totalRulesCount')).to.be('Showing: 2 of 2 rules.'); + expect(await testSubjects.getVisibleText('totalActiveRulesCount')).to.be('Active: 0'); + expect(await testSubjects.getVisibleText('totalOkRulesCount')).to.be('Ok: 1'); + expect(await testSubjects.getVisibleText('totalErrorRulesCount')).to.be('Error: 1'); + expect(await testSubjects.getVisibleText('totalPendingRulesCount')).to.be('Pending: 0'); + expect(await testSubjects.getVisibleText('totalUnknownRulesCount')).to.be('Unknown: 0'); }); it('should filter alerts by the alert type', async () => { await createAlert({ supertest, objectRemover }); const failingAlert = await createFailingAlert({ supertest, objectRemover }); await refreshAlertsList(); - await testSubjects.click('alertTypeFilterButton'); - expect(await (await testSubjects.find('alertType0Group')).getVisibleText()).to.eql('Alerts'); - await testSubjects.click('alertTypetest.failingFilterOption'); + await testSubjects.click('ruleTypeFilterButton'); + expect(await (await testSubjects.find('ruleType0Group')).getVisibleText()).to.eql('Alerts'); + await testSubjects.click('ruleTypetest.failingFilterOption'); await retry.try(async () => { const filterFailingAlertOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); @@ -529,7 +529,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(filterWithSlackOnlyResults[0].interval).to.equal('1 min'); expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/); }); - await testSubjects.click('alertTypeFilterButton'); + await testSubjects.click('ruleTypeFilterButton'); // de-select action type filter await testSubjects.click('actionTypeFilterButton'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index d350e88bcf2488..b280e9a3e78c56 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -124,7 +124,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first alert await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); @@ -260,20 +260,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(ruleName); - const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + const editButton = await testSubjects.find('openEditRuleFlyoutButton'); await editButton.click(); expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); - await testSubjects.setValue('alertNameInput', updatedRuleName, { + await testSubjects.setValue('ruleNameInput', updatedRuleName, { clearWithKeyboard: true, }); - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); + await find.clickByCssSelector('[data-test-subj="saveEditedRuleButton"]:not(disabled)'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Updated '${updatedRuleName}'`); @@ -291,26 +291,26 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(updatedRuleName); - const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + const editButton = await testSubjects.find('openEditRuleFlyoutButton'); await editButton.click(); - await testSubjects.setValue('alertNameInput', uuid.v4(), { + await testSubjects.setValue('ruleNameInput', uuid.v4(), { clearWithKeyboard: true, }); - await testSubjects.click('cancelSaveEditedAlertButton'); - await testSubjects.existOrFail('confirmAlertCloseModal'); - await testSubjects.click('confirmAlertCloseModal > confirmModalConfirmButton'); - await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); + await testSubjects.click('cancelSaveEditedRuleButton'); + await testSubjects.existOrFail('confirmRuleCloseModal'); + await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton'); + await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedRuleButton"]'); await editButton.click(); - const nameInputAfterCancel = await testSubjects.find('alertNameInput'); + const nameInputAfterCancel = await testSubjects.find('ruleNameInput'); const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); expect(textAfterCancel).to.eql(updatedRuleName); }); @@ -345,7 +345,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // delete connector await pageObjects.triggersActionsUI.changeTabs('connectorsTab'); @@ -362,7 +362,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.changeTabs('rulesTab'); await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); - const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + const editButton = await testSubjects.find('openEditRuleFlyoutButton'); await editButton.click(); expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); @@ -409,7 +409,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // delete connector await pageObjects.triggersActionsUI.changeTabs('connectorsTab'); @@ -426,7 +426,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.changeTabs('rulesTab'); await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); - const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + const editButton = await testSubjects.find('openEditRuleFlyoutButton'); await editButton.click(); expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); @@ -479,7 +479,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); @@ -501,7 +501,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); @@ -527,7 +527,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); @@ -735,7 +735,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first rule await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 8ebb720930364b..255a0d6c09bee2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -79,7 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(url).to.contain(`/rules`); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); }); it('navigates to an alert details page', async () => { @@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content - await testSubjects.existOrFail('alertsList'); + await testSubjects.existOrFail('rulesList'); // click on first alert await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(createdAlert.name); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 1d8a172e57b78b..2ad2da6d227c20 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -88,7 +88,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can save alert', async () => { - await alerts.clickSaveAlertButton(); + await alerts.clickSaveRuleButton(); await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); @@ -178,7 +178,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can save alert', async () => { - await alerts.clickSaveAlertButton(); + await alerts.clickSaveRuleButton(); await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts index 9e958d4207e56d..01d7c24be2f416 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts @@ -16,10 +16,10 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) { return { async getHeadingText() { - return await testSubjects.getVisibleText('alertDetailsTitle'); + return await testSubjects.getVisibleText('ruleDetailsTitle'); }, async getRuleType() { - return await testSubjects.getVisibleText('alertTypeLabel'); + return await testSubjects.getVisibleText('ruleTypeLabel'); }, async getActionsLabels() { return { @@ -98,20 +98,20 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) { }, async isViewInAppDisabled() { await retry.try(async () => { - const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`); + const viewInAppButton = await testSubjects.find(`ruleDetails-viewInApp`); expect(await viewInAppButton.getAttribute('disabled')).to.eql('true'); }); return true; }, async isViewInAppEnabled() { await retry.try(async () => { - const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`); + const viewInAppButton = await testSubjects.find(`ruleDetails-viewInApp`); expect(await viewInAppButton.getAttribute('disabled')).to.not.eql('true'); }); return true; }, async clickViewInApp() { - return await testSubjects.click('alertDetails-viewInApp'); + return await testSubjects.click('ruleDetails-viewInApp'); }, async getNoOpAppTitle() { await retry.try(async () => { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a49873c6d47b53..c715800abd37e3 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -21,17 +21,17 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) { return { - name: $(row).findTestSubject('alertsTableCell-name').find('.euiTableCellContent').text(), + name: $(row).findTestSubject('rulesTableCell-name').find('.euiTableCellContent').text(), duration: $(row) - .findTestSubject('alertsTableCell-duration') + .findTestSubject('rulesTableCell-duration') .find('.euiTableCellContent') .text(), interval: $(row) - .findTestSubject('alertsTableCell-interval') + .findTestSubject('rulesTableCell-interval') .find('.euiTableCellContent') .text(), tags: $(row) - .findTestSubject('alertsTableCell-tagsPopover') + .findTestSubject('rulesTableCell-tagsPopover') .find('.euiTableCellContent') .text(), }; @@ -68,13 +68,13 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) ); }, async searchAlerts(searchText: string) { - const searchBox = await testSubjects.find('alertSearchField'); + const searchBox = await testSubjects.find('ruleSearchField'); await searchBox.click(); await searchBox.clearValue(); await searchBox.type(searchText); await searchBox.pressKeys(ENTER_KEY); await find.byCssSelector( - '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' + '.euiBasicTable[data-test-subj="rulesList"]:not(.euiBasicTable-loading)' ); }, async getConnectorsList() { @@ -96,42 +96,42 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }); }, async getAlertsList() { - const table = await find.byCssSelector('[data-test-subj="alertsList"] table'); + const table = await find.byCssSelector('[data-test-subj="rulesList"] table'); const $ = await table.parseDomContent(); - return $.findTestSubjects('alert-row') + return $.findTestSubjects('rule-row') .toArray() .map((row) => { return getRowItemData(row, $); }); }, async getAlertsListWithStatus() { - const table = await find.byCssSelector('[data-test-subj="alertsList"] table'); + const table = await find.byCssSelector('[data-test-subj="rulesList"] table'); const $ = await table.parseDomContent(); - return $.findTestSubjects('alert-row') + return $.findTestSubjects('rule-row') .toArray() .map((row) => { const rowItem = getRowItemData(row, $); return { ...rowItem, status: $(row) - .findTestSubject('alertsTableCell-status') + .findTestSubject('rulesTableCell-status') .find('.euiTableCellContent') .text(), }; }); }, async isAlertsListDisplayed() { - const table = await find.byCssSelector('[data-test-subj="alertsList"] table'); + const table = await find.byCssSelector('[data-test-subj="rulesList"] table'); return table.isDisplayed(); }, async isAnEmptyAlertsListDisplayed() { await retry.try(async () => { - const table = await find.byCssSelector('[data-test-subj="alertsList"] table'); + const table = await find.byCssSelector('[data-test-subj="rulesList"] table'); const $ = await table.parseDomContent(); - const rows = $.findTestSubjects('alert-row').toArray(); + const rows = $.findTestSubjects('rule-row').toArray(); expect(rows.length).to.eql(0); const emptyRow = await find.byCssSelector( - '[data-test-subj="alertsList"] table .euiTableRow' + '[data-test-subj="rulesList"] table .euiTableRow' ); expect(await emptyRow.getVisibleText()).to.eql('No items found'); }); @@ -139,7 +139,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }, async clickOnAlertInAlertsList(name: string) { await this.searchAlerts(name); - await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + await find.clickDisplayedByCssSelector(`[data-test-subj="rulesList"] [title="${name}"]`); }, async changeTabs(tab: 'rulesTab' | 'connectorsTab') { await testSubjects.click(tab); @@ -150,16 +150,16 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }, async clickCreateAlertButton() { const createBtn = await find.byCssSelector( - '[data-test-subj="createAlertButton"],[data-test-subj="createFirstAlertButton"]' + '[data-test-subj="createRuleButton"],[data-test-subj="createFirstRuleButton"]' ); await createBtn.click(); }, async setAlertName(value: string) { - await testSubjects.setValue('alertNameInput', value); + await testSubjects.setValue('ruleNameInput', value); await this.assertAlertName(value); }, async assertAlertName(expectedValue: string) { - const actualValue = await testSubjects.getAttribute('alertNameInput', 'value'); + const actualValue = await testSubjects.getAttribute('ruleNameInput', 'value'); expect(actualValue).to.eql(expectedValue); }, async setAlertInterval(value: number, unit?: 's' | 'm' | 'h' | 'd') { @@ -178,8 +178,8 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) } }, async saveAlert() { - await testSubjects.click('saveAlertButton'); - const isConfirmationModalVisible = await testSubjects.isDisplayed('confirmAlertSaveModal'); + await testSubjects.click('saveRuleButton'); + const isConfirmationModalVisible = await testSubjects.isDisplayed('confirmRuleSaveModal'); expect(isConfirmationModalVisible).to.eql(true, 'Expect confirmation modal to be visible'); await testSubjects.click('confirmModalConfirmButton'); }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts b/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts new file mode 100644 index 00000000000000..2ddeeb711600ed --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.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 expect from '@kbn/expect'; +import { ReportApiJSON } from '../../../plugins/reporting/common/types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const reportingAPI = getService('reportingAPI'); + + describe('Reporting error codes', () => { + it('places error_code in report output', async () => { + await reportingAPI.initEcommerce(); + + const { body: reportApiJson, status } = await reportingAPI.generateCsv({ + title: 'CSV Report', + browserTimezone: 'UTC', + objectType: 'search', + version: '7.15.0', + searchSource: null, // Invalid searchSource that should cause job to throw at execute phase... + } as any); + expect(status).to.be(200); + + const { job: report, path: downloadPath } = reportApiJson as { + job: ReportApiJSON; + path: string; + }; + + // wait for the the pending job to complete + await reportingAPI.waitForJobToFinish(downloadPath, true); + + expect(await reportingAPI.getJobErrorCode(report.id)).to.be('unknown_error'); + + await reportingAPI.teardownEcommerce(); + await reportingAPI.deleteAllReports(); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 02a2915fffd604..4cff15dc9f4443 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -29,5 +29,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./usage')); loadTestFile(require.resolve('./ilm_migration_apis')); + loadTestFile(require.resolve('./error_codes')); }); } diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index 20099eb2eb1e1a..be72233a26ffc3 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -164,6 +164,7 @@ export function createScenarios({ getService }: Pick { const jobParams = rison.encode(job as object as RisonValue); + return await supertestWithoutAuth .post(`/api/reporting/generate/csv_searchsource`) .auth(username, password) @@ -191,6 +192,22 @@ export function createScenarios({ getService }: Pick => { + const { + body: [job], + } = await supertestWithoutAuth + .get(`/api/reporting/jobs/list?page=0&ids=${id}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + return job?.output?.error_code; + }; + const deleteAllReports = async () => { log.debug('ReportingAPI.deleteAllReports'); @@ -271,5 +288,6 @@ export function createScenarios({ getService }: Pick { - // FAILING: https://github.com/elastic/kibana/issues/110153 - describe.skip('rules security and spaces enabled: basic', function () { + describe('rules security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); @@ -24,10 +23,12 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { }); // Basic - loadTestFile(require.resolve('./get_alert_by_id')); - loadTestFile(require.resolve('./update_alert')); - loadTestFile(require.resolve('./bulk_update_alerts')); - loadTestFile(require.resolve('./find_alerts')); - loadTestFile(require.resolve('./get_alerts_index')); + // FAILING: https://github.com/elastic/kibana/issues/110153 + // loadTestFile(require.resolve('./get_alert_by_id')); + // loadTestFile(require.resolve('./update_alert')); + // loadTestFile(require.resolve('./bulk_update_alerts')); + // loadTestFile(require.resolve('./find_alerts')); + // loadTestFile(require.resolve('./get_alerts_index')); + loadTestFile(require.resolve('./search_strategy')); }); }; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts new file mode 100644 index 00000000000000..2124cb8a1d04ba --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { AlertConsumers } from '@kbn/rule-data-utils'; + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { RuleRegistrySearchResponse } from '../../../../../plugins/rule_registry/common/search_strategy'; +import { + deleteSignalsIndex, + createSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + createRule, + waitForSignalsToBePresent, + waitForRuleSuccessOrStatus, +} from '../../../../detection_engine_api_integration/utils'; +import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/tests/generating_signals'; +import { QueryCreateSchema } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const bsearch = getService('bsearch'); + const log = getService('log'); + + const SPACE1 = 'space1'; + + describe('ruleRegistryAlertsSearchStrategy', () => { + describe('logs', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + it('should return alerts from log rules', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.LOGS], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + }); + expect(result.rawResponse.hits.total).to.eql(5); + const consumers = result.rawResponse.hits.hits.map((hit) => { + return hit.fields?.['kibana.alert.rule.consumer']; + }); + expect(consumers.every((consumer) => consumer === AlertConsumers.LOGS)); + }); + }); + + describe('siem', () => { + beforeEach(async () => { + await deleteSignalsIndex(supertest, log); + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + it('should return alerts from siem rules', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { id: createdId } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, createdId); + await waitForSignalsToBePresent(supertest, log, 1, [createdId]); + + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.SIEM], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + }); + expect(result.rawResponse.hits.total).to.eql(1); + const consumers = result.rawResponse.hits.hits.map( + (hit) => hit.fields?.['kibana.alert.rule.consumer'] + ); + expect(consumers.every((consumer) => consumer === AlertConsumers.SIEM)); + }); + }); + + describe('apm', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return alerts from apm rules', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.APM], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + space: SPACE1, + }); + expect(result.rawResponse.hits.total).to.eql(2); + const consumers = result.rawResponse.hits.hits.map( + (hit) => hit.fields?.['kibana.alert.rule.consumer'] + ); + expect(consumers.every((consumer) => consumer === AlertConsumers.APM)); + }); + }); + + describe('empty response', () => { + it('should return an empty response', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + space: SPACE1, + }); + expect(result.rawResponse).to.eql({}); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts index ffa7473e954164..5e1166d03f0d55 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts @@ -11,7 +11,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../security_solution_endpoint/services/endpoint_artifacts'; -import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../plugins/security_solution/common/endpoint/service/artifacts'; +import { + BY_POLICY_ARTIFACT_TAG_PREFIX, + GLOBAL_ARTIFACT_TAG, +} from '../../../../plugins/security_solution/common/endpoint/service/artifacts'; import { createUserAndRole, deleteUserAndRole, @@ -29,6 +32,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const endpointPolicyTestResources = getService('endpointPolicyTestResources'); const endpointArtifactTestResources = getService('endpointArtifactTestResources'); + const log = getService('log'); type ApiCallsInterface = Array<{ method: keyof Pick; @@ -67,7 +71,10 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: EXCEPTION_LIST_ITEM_URL, - getBody: () => exceptionsGenerator.generateHostIsolationExceptionForCreate(), + getBody: () => + exceptionsGenerator.generateHostIsolationExceptionForCreate({ + tags: [GLOBAL_ARTIFACT_TAG], + }), }, { method: 'put', @@ -77,6 +84,7 @@ export default function ({ getService }: FtrProviderContext) { id: existingExceptionData.artifact.id, item_id: existingExceptionData.artifact.item_id, _version: existingExceptionData.artifact._version, + tags: [GLOBAL_ARTIFACT_TAG], }), }, ]; @@ -214,11 +222,17 @@ export default function ({ getService }: FtrProviderContext) { await supertest[apiCall.method](apiCall.path) .set('kbn-xsrf', 'true') + .on('error', (error) => { + log.error(JSON.stringify(error?.response?.body ?? error, null, 2)); + }) .send(body) .expect(200); const deleteUrl = `${EXCEPTION_LIST_ITEM_URL}?item_id=${body.item_id}&namespace_type=${body.namespace_type}`; - await supertest.delete(deleteUrl).set('kbn-xsrf', 'true'); + + log.info(`cleaning up: ${deleteUrl}`); + + await supertest.delete(deleteUrl).set('kbn-xsrf', 'true').expect(200); }); } }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts index 7caf4f085694a7..60b479c5b37d9e 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts @@ -11,7 +11,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../security_solution_endpoint/services//endpoint_artifacts'; -import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../plugins/security_solution/common/endpoint/service/artifacts'; +import { + BY_POLICY_ARTIFACT_TAG_PREFIX, + GLOBAL_ARTIFACT_TAG, +} from '../../../../plugins/security_solution/common/endpoint/service/artifacts'; import { ExceptionsListItemGenerator } from '../../../../plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator'; import { createUserAndRole, @@ -91,7 +94,9 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: EXCEPTION_LIST_ITEM_URL, - getBody: () => exceptionsGenerator.generateTrustedAppForCreate(), + getBody: () => { + return exceptionsGenerator.generateTrustedAppForCreate({ tags: [GLOBAL_ARTIFACT_TAG] }); + }, }, { method: 'put', @@ -100,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) { exceptionsGenerator.generateTrustedAppForUpdate({ id: trustedAppData.artifact.id, item_id: trustedAppData.artifact.item_id, + tags: [GLOBAL_ARTIFACT_TAG], }), }, ]; diff --git a/yarn.lock b/yarn.lock index 827fe1a7eae40f..68816f9b172a92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2142,6 +2142,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -2371,12 +2376,12 @@ dependencies: "@elastic/ecs-helpers" "^1.1.0" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.1.0-canary.2": - version "8.1.0-canary.2" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.1.0-canary.2.tgz#7676b3bdad79a37be4b4ada38f97751314a33a52" - integrity sha512-nmr7yZbvlTqA5SHu/IJZFsU6v14+Y2nx0btMKB9Hjd0vardaibCAdovO9Bp1RPxda2g6XayEkKEzwq5s79xR1g== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.1.0-canary.3": + version "8.1.0-canary.3" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.1.0-canary.3.tgz#a84669ad45ea465e533d860bf99aa55aed781cb3" + integrity sha512-rpsMiJX5sAAlPjfWzZhijQgpu7ZlPwjcJQHCT3wNz03DTDnokLCqkhc8gsU+uqesbQ/GqYUlSL9erCk4GqjOLg== dependencies: - "@elastic/transport" "^8.1.0-beta.1" + "@elastic/transport" "^8.0.2" tslib "^2.3.0" "@elastic/ems-client@8.0.0": @@ -2559,17 +2564,17 @@ ts-node "^10.2.1" typescript "^4.3.5" -"@elastic/transport@^8.1.0-beta.1": - version "8.1.0-beta.1" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.1.0-beta.1.tgz#37fde777cf83226f1ea46bf0a22e51a3e43efb85" - integrity sha512-aqncMX86d3r6tNGlve6HEy+NF8XZXetMxDXpplrOAcShL20mHXkMFTJyUyML01tgfkbbgwXnN714YEjin1u1Xg== +"@elastic/transport@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.0.2.tgz#715f06c7739516867508108df30c33973ca8e81c" + integrity sha512-OlDz3WO3pKE9vSxW4wV/mn7rYCtBmSsDwxr64h/S1Uc/zrIBXb0iUsRMSkiybXugXhjwyjqG2n1Wc7jjFxrskQ== dependencies: debug "^4.3.2" hpagent "^0.1.2" ms "^2.1.3" secure-json-parse "^2.4.0" tslib "^2.3.0" - undici "^4.7.0" + undici "^4.14.1" "@emotion/babel-plugin-jsx-pragmatic@^0.1.5": version "0.1.5" @@ -9587,10 +9592,10 @@ bach@^1.0.0: async-settle "^1.0.0" now-and-later "^2.0.0" -backport@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/backport/-/backport-7.0.1.tgz#021f70db76b89699b2c7b826cb3040e9c1d991c9" - integrity sha512-f/7+NDzLFd307c85Tz60cfBzoRd4HlFlNOm3MYFynQwI4igMmKd4J9bFxLgc3KdToaVWDmZ37Gx9nRkYgMfUkA== +backport@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/backport/-/backport-7.3.1.tgz#c5c57e03c87f5883f769e30efc0e7193ce7670f2" + integrity sha512-F1gjJx/pxn9zI74Np6FlTD8ovqeUbzzgzGyBpoYypAdDTJG8Vxt1jcrcwZtRIaSd7McfXCSoQsinlFzO4qlPcA== dependencies: "@octokit/rest" "^18.12.0" axios "^0.25.0" @@ -9608,7 +9613,7 @@ backport@7.0.1: strip-json-comments "^3.1.1" terminal-link "^2.1.1" utility-types "^3.10.0" - winston "^3.5.1" + winston "^3.6.0" yargs "^17.3.1" yargs-parser "^21.0.0" @@ -11595,10 +11600,10 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.4, core-js@^3.21.0, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: - version "3.21.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.0.tgz#f479dbfc3dffb035a0827602dd056839a774aa71" - integrity sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ== +core-js@^3.0.4, core-js@^3.21.1, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" + integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -20101,6 +20106,17 @@ logform@^2.3.2: safe-stable-stringify "^1.1.0" triple-beam "^1.3.0" +logform@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" + integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + loglevel@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" @@ -29147,10 +29163,10 @@ undertaker@^1.2.1: object.reduce "^1.0.0" undertaker-registry "^1.0.0" -undici@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-4.7.0.tgz#3bda286d67bf45d0ab1b94ca6c84e546dcb3b0d4" - integrity sha512-O1q+/EIs4g0HnVMH8colei3qODGiYBLpavWYv3kI+JazBBsBIndnZfUqZ2MEfPJ12H9d56yVdwZG1/nV/xcoSQ== +undici@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-4.14.1.tgz#7633b143a8a10d6d63335e00511d071e8d52a1d9" + integrity sha512-WJ+g+XqiZcATcBaUeluCajqy4pEDcQfK1vy+Fo+bC4/mqXI9IIQD/XWHLS70fkGUT6P52Drm7IFslO651OdLPQ== unfetch@^4.2.0: version "4.2.0" @@ -29585,7 +29601,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3: +url-parse@^1.4.3, url-parse@^1.5.6: version "1.5.9" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.9.tgz#05ff26484a0b5e4040ac64dcee4177223d74675e" integrity sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ== @@ -29593,14 +29609,6 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" -url-parse@^1.5.6: - version "1.5.6" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.6.tgz#b2a41d5a233645f3c31204cc8be60e76a15230a2" - integrity sha512-xj3QdUJ1DttD1LeSfvJlU1eiF1RvBSBfUu8GplFGdUzSO28y5yUtEl7wb//PI4Af6qh0o/K8545vUmucRrfWsw== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - url-template@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" @@ -30816,7 +30824,7 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" -winston-transport@^4.4.2: +winston-transport@^4.4.2, winston-transport@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== @@ -30825,7 +30833,7 @@ winston-transport@^4.4.2: readable-stream "^3.6.0" triple-beam "^1.3.0" -winston@^3.0.0, winston@^3.3.3, winston@^3.5.1: +winston@^3.0.0, winston@^3.3.3: version "3.5.1" resolved "https://registry.yarnpkg.com/winston/-/winston-3.5.1.tgz#b25cc899d015836dbf8c583dec8c4c4483a0da2e" integrity sha512-tbRtVy+vsSSCLcZq/8nXZaOie/S2tPXPFt4be/Q3vI/WtYwm7rrwidxVw2GRa38FIXcJ1kUM6MOZ9Jmnk3F3UA== @@ -30841,6 +30849,22 @@ winston@^3.0.0, winston@^3.3.3, winston@^3.5.1: triple-beam "^1.3.0" winston-transport "^4.4.2" +winston@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6" + integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + wkt-parser@^1.2.4: version "1.3.2" resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.3.2.tgz#deeff04a21edc5b170a60da418e9ed1d1ab0e219"