diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index 0eccb55cef830..b14a3be8f8daf 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -50,6 +50,9 @@ for (const deployment of deploymentsToPurge) { console.log(`Scheduling deployment for deletion: ${deployment.name} / ${deployment.id}`); try { execSync(`ecctl deployment shutdown --force '${deployment.id}'`, { stdio: 'inherit' }); + execSync(`vault delete secret/kibana-issues/dev/cloud-deploy/${deployment.name}`, { + stdio: 'inherit', + }); } catch (ex) { console.error(ex.toString()); } diff --git a/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh b/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh index 44d4bb500e1d2..7a54e770b8a3e 100755 --- a/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh +++ b/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh @@ -20,10 +20,19 @@ export CODE_COVERAGE=1 echo "--- OSS CI Group $CI_GROUP" echo " -> Running Functional tests with code coverage" -node scripts/functional_tests \ +NODE_OPTIONS=--max_old_space_size=14336 \ + ./node_modules/.bin/nyc \ + --nycrc-path src/dev/code_coverage/nyc_config/nyc.server.config.js \ + node scripts/functional_tests \ --include-tag "ciGroup$CI_GROUP" \ --exclude-tag "skipCoverage" || true +if [[ -d "$KIBANA_DIR/target/kibana-coverage/server" ]]; then + echo "--- Server side code coverage collected" + mkdir -p target/kibana-coverage/functional + mv target/kibana-coverage/server/coverage-final.json "target/kibana-coverage/functional/oss-${CI_GROUP}-server-coverage.json" +fi + if [[ -d "$KIBANA_DIR/target/kibana-coverage/functional" ]]; then echo "--- Merging code coverage for CI Group $CI_GROUP" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js --reporter json diff --git a/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh b/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh index c85191e4e4632..a3fdff6690485 100755 --- a/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh +++ b/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh @@ -22,12 +22,21 @@ echo " -> Running X-Pack functional tests with code coverage" cd "$XPACK_DIR" -node scripts/functional_tests \ +NODE_OPTIONS=--max_old_space_size=14336 \ + ./../node_modules/.bin/nyc \ + --nycrc-path ./../src/dev/code_coverage/nyc_config/nyc.server.config.js \ + node scripts/functional_tests \ --include-tag "ciGroup$CI_GROUP" \ --exclude-tag "skipCoverage" || true cd "$KIBANA_DIR" +if [[ -d "$KIBANA_DIR/target/kibana-coverage/server" ]]; then + echo "--- Server side code coverage collected" + mkdir -p target/kibana-coverage/functional + mv target/kibana-coverage/server/coverage-final.json "target/kibana-coverage/functional/xpack-${CI_GROUP}-server-coverage.json" +fi + if [[ -d "$KIBANA_DIR/target/kibana-coverage/functional" ]]; then echo "--- Merging code coverage for CI Group $CI_GROUP" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js --reporter json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 604179ec75706..3f36f4b67e56b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -268,6 +268,7 @@ /packages/kbn-std/ @elastic/kibana-core /packages/kbn-config/ @elastic/kibana-core /packages/kbn-logging/ @elastic/kibana-core +/packages/kbn-logging-mocks/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml new file mode 100644 index 0000000000000..fc5676887f3ae --- /dev/null +++ b/.github/workflows/add-to-fleet-project.yml @@ -0,0 +1,32 @@ +name: Add to Fleet:Quality project +on: + issues: + types: + - labeled +jobs: + add_to_project: + runs-on: ubuntu-latest + if: | + contains(github.event.issue.labels.*.name, 'Team:Fleet') && ( + contains(github.event.issue.labels.*.name, 'technical debt') || + contains(github.event.issue.labels.*.name, 'bug') || + contains(github.event.issue.labels.*.name, 'performance') + ) + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:String!,$contentid:String!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAGc3Zs4AAsH6" + GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc deleted file mode 100644 index ccd3f41b9d886..0000000000000 --- a/docs/management/upgrade-assistant/index.asciidoc +++ /dev/null @@ -1,26 +0,0 @@ -[role="xpack"] -[[upgrade-assistant]] -== Upgrade Assistant - -The Upgrade Assistant helps you prepare for your upgrade -to the next major version of the Elastic Stack. -To access the assistant, open the main menu and go to *Stack Management > Upgrade Assistant*. - -The assistant identifies deprecated settings in your configuration, -enables you to see if you are using deprecated features, -and guides you through the process of resolving issues. - -If you have indices that were created prior to 7.0, -you can use the assistant to reindex them so they can be accessed from 8.0+. - -IMPORTANT: To see the most up-to-date deprecation information before -upgrading to 8.0, upgrade to the latest {prev-major-last} release. - -For more information about upgrading, -refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] - -[discrete] -=== Required permissions - -The `manage` cluster privilege is required to access the *Upgrade assistant*. -Additional privileges may be needed to perform certain actions. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index ff6ccbd6fab36..0ca518c3a8788 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -386,12 +386,17 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. -[role="exclude" logging-configuration-changes] -== Logging configuration changes +[role="exclude",id="logging-configuration-changes"] +== Logging configuration changes This content has moved. Refer to <>. -[role="exclude" upgrade-migrations] +[role="exclude",id="upgrade-migrations"] == Upgrade migrations This content has moved. Refer to <>. + +[role="exclude",id="upgrade-assistant"] +== Upgrade Assistant + +This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index d8e08b460e5f6..4eabfa0c07714 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,7 @@ == Upgrade {kib} To upgrade from 7.16 or earlier to {version}, -**You must first upgrade to {prev-major-last}**. +**you must first upgrade to {prev-major-last}**. This enables you to use the Upgrade Assistant to {stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare to upgrade]. You must resolve all critical issues identified by the Upgrade Assistant diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index fc921f9118bdf..7136011a4f8f8 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -15,11 +15,11 @@ WARNING: The following instructions assumes {kib} is using the default index nam [[upgrade-migrations-process]] ==== Background -Saved objects are stored in two indices: +Saved objects are stored in two indices: * `.kibana_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. * `.kibana_task_manager_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. - + The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date saved object indices. @@ -29,18 +29,18 @@ The first time a newer {kib} starts, it will first perform an upgrade migration [options="header"] |======================= |Upgrading from version | Outdated index (alias) -| 6.0.0 through 6.4.x | `.kibana` +| 6.0.0 through 6.4.x | `.kibana` `.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) | 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) -| 7.4.0 through 7.11.x -| `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) `.kibana_task_manager_N` (`.kibana_task_manager` alias) |======================= ==== Upgrading multiple {kib} instances -When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. @@ -64,13 +64,15 @@ Error: Unable to complete saved object migrations for the [.kibana] index. Pleas Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] -------------------------------------------- -See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. - +Instructions to work around this issue are in https://github.com/elastic/kibana/issues/95321[this GitHub issue]. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. -Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +Saved objects that were corrupted through manual editing or integrations will cause migration +failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. +Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. For example, given the following error message: @@ -101,7 +103,7 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab -------------------------------------------- . Restart {kib}. - ++ In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. @@ -112,15 +114,16 @@ Matching index templates which specify `settings.refresh_interval` or `mappings` Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. -Note: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and index pattern `.kibana`. This index template will not interfere and does not need to be changed or removed. +NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` +and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. [float] ===== An unhealthy {es} cluster Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: - * enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices - * sufficient heap size - * a "green" cluster status + * Enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices + * Sufficient heap size + * A "green" cluster status [float] ===== Different versions of {kib} connected to the same {es} index @@ -134,20 +137,32 @@ For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to +perform the migration again once the process has restarted. Do not delete any saved objects indices to +attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and +later does not require deleting any indices to release a failed migration lock. -If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. +If upgrade migrations fail repeatedly, follow the advice in +<>. +Once the root cause for the migration failure has been addressed, +{kib} will automatically retry the migration without any further intervention. +If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. +If you've followed the advice in <> +and <> and +{kib} is still not able to upgrade successfully, you might choose to rollback {kib} until +you're able to identify and fix the root cause. -WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + +WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with +your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be +rolled back to be compatible with the previous {kib} version. [float] ===== Rollback by restoring a backup snapshot: @@ -164,8 +179,11 @@ In order to rollback after a failed upgrade migration, the saved object indices 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. -3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` -4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. For example, if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. +If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. +Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. +For example. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. 5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e682f7372f817..6c309d56f2294 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -167,10 +167,6 @@ set the timespan for notification messages, and much more. the full list of features that are included in your license, see the https://www.elastic.co/subscriptions[subscription page]. -| <> -| Identify the issues that you need to address before upgrading to the -next major version of {es}, and then reindex, if needed. - |=== @@ -197,6 +193,4 @@ include::{kib-repo-dir}/spaces/index.asciidoc[] include::{kib-repo-dir}/management/managing-tags.asciidoc[] -include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] - include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] diff --git a/package.json b/package.json index 8738fdb8e0f9f..9ea3e8335f886 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", + "@kbn/logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", @@ -454,6 +455,7 @@ "@elastic/synthetics": "^1.0.0-beta.16", "@emotion/babel-preset-css-prop": "^11.2.0", "@emotion/jest": "^11.3.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/schema": "^0.1.2", "@jest/console": "^26.6.2", "@jest/reporters": "^26.6.2", @@ -586,6 +588,8 @@ "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/kbn__interpreter": "link:bazel-bin/packages/kbn-interpreter/npm_module_types", "@types/kbn__io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types", + "@types/kbn__logging": "link:bazel-bin/packages/kbn-logging/npm_module_types", + "@types/kbn__logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks/npm_module_types", "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 7e6d06922aed2..6421f36bf73b7 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -33,6 +33,7 @@ filegroup( "//packages/kbn-interpreter:build", "//packages/kbn-io-ts-utils:build", "//packages/kbn-logging:build", + "//packages/kbn-logging-mocks:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", "//packages/kbn-optimizer:build", @@ -101,6 +102,8 @@ filegroup( "//packages/kbn-i18n-react:build_types", "//packages/kbn-interpreter:build_types", "//packages/kbn-io-ts-utils:build_types", + "//packages/kbn-logging:build_types", + "//packages/kbn-logging-mocks:build_types", "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 87d4a116f13b1..133474a3aefa6 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -51,7 +51,7 @@ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", "//packages/kbn-dev-utils:npm_module_types", - "//packages/kbn-logging", + "//packages/kbn-logging:npm_module_types", "//packages/kbn-optimizer:npm_module_types", "//packages/kbn-server-http-tools:npm_module_types", "//packages/kbn-std:npm_module_types", diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index d7046a26ff92f..0577014768d4c 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -34,6 +34,7 @@ RUNTIME_DEPS = [ "//packages/elastic-safer-lodash-set", "//packages/kbn-config-schema", "//packages/kbn-logging", + "//packages/kbn-logging-mocks", "//packages/kbn-std", "//packages/kbn-utility-types", "//packages/kbn-i18n", @@ -47,7 +48,8 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/elastic-safer-lodash-set:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-logging", + "//packages/kbn-logging:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", "//packages/kbn-std:npm_module_types", "//packages/kbn-utility-types:npm_module_types", "//packages/kbn-i18n:npm_module_types", diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index 32b2d8969d0cc..51e67956637ee 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -13,7 +13,7 @@ import { mockApplyDeprecations, mockedChangedPaths } from './config_service.test import { rawConfigServiceMock } from './raw/raw_config_service.mock'; import { schema } from '@kbn/config-schema'; -import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; import type { ConfigDeprecationContext } from './deprecation'; import { ConfigService, Env, RawPackageInfo } from '.'; diff --git a/packages/kbn-logging-mocks/BUILD.bazel b/packages/kbn-logging-mocks/BUILD.bazel new file mode 100644 index 0000000000000..74fb9c2651e5d --- /dev/null +++ b/packages/kbn-logging-mocks/BUILD.bazel @@ -0,0 +1,106 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_BASE_NAME = "kbn-logging-mocks" +PKG_REQUIRE_NAME = "@kbn/logging-mocks" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging-mocks" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "//packages/kbn-logging" +] + +TYPES_DEPS = [ + "//packages/kbn-logging:npm_module_types", + "@npm//@types/jest", + "@npm//@types/node", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + 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", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-logging-mocks/package.json b/packages/kbn-logging-mocks/package.json new file mode 100644 index 0000000000000..789ffe4500bce --- /dev/null +++ b/packages/kbn-logging-mocks/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/logging-mocks", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target_node/index.js" +} \ No newline at end of file diff --git a/packages/kbn-logging/src/mocks/index.ts b/packages/kbn-logging-mocks/src/index.ts similarity index 100% rename from packages/kbn-logging/src/mocks/index.ts rename to packages/kbn-logging-mocks/src/index.ts diff --git a/packages/kbn-logging/src/mocks/logger.mock.ts b/packages/kbn-logging-mocks/src/logger.mock.ts similarity index 97% rename from packages/kbn-logging/src/mocks/logger.mock.ts rename to packages/kbn-logging-mocks/src/logger.mock.ts index 1b9cdcf71bfa1..b5f1f409ee457 100644 --- a/packages/kbn-logging/src/mocks/logger.mock.ts +++ b/packages/kbn-logging-mocks/src/logger.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Logger } from '../logger'; +import { Logger } from '@kbn/logging'; export type MockedLogger = jest.Mocked & { context: string[] }; diff --git a/packages/kbn-logging-mocks/tsconfig.json b/packages/kbn-logging-mocks/tsconfig.json new file mode 100644 index 0000000000000..ce53e016c2830 --- /dev/null +++ b/packages/kbn-logging-mocks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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", + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/kbn-logging/BUILD.bazel b/packages/kbn-logging/BUILD.bazel index 8e55456069ee4..09ff3f0d83b2d 100644 --- a/packages/kbn-logging/BUILD.bazel +++ b/packages/kbn-logging/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-logging" PKG_REQUIRE_NAME = "@kbn/logging" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging" SOURCE_FILES = glob( [ @@ -22,7 +23,6 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "mocks/package.json", "package.json", "README.md" ] @@ -69,7 +69,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -88,3 +88,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-logging/mocks/package.json b/packages/kbn-logging/mocks/package.json deleted file mode 100644 index 8410f557e9524..0000000000000 --- a/packages/kbn-logging/mocks/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "private": true, - "main": "../target_node/mocks/index.js", - "types": "../target_types/mocks/index.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-logging/package.json b/packages/kbn-logging/package.json index c35c2f5d06095..0220da8709d30 100644 --- a/packages/kbn-logging/package.json +++ b/packages/kbn-logging/package.json @@ -3,6 +3,5 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target_node/index.js", - "types": "./target_types/index.d.ts" + "main": "./target_node/index.js" } \ No newline at end of file diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index b286cf05a6d71..6cfd1d57e9c96 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -// eslint-disable-next-line no-restricted-syntax -const alwaysImportedTests = [ +require('../src/setup_node_env'); +require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), @@ -25,18 +25,7 @@ const alwaysImportedTests = [ require.resolve( '../test/interactive_setup_functional/manual_configuration_without_tls.config.ts' ), -]; -// eslint-disable-next-line no-restricted-syntax -const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), -]; - -require('../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - // eslint-disable-next-line no-restricted-syntax - ...alwaysImportedTests, - // eslint-disable-next-line no-restricted-syntax - ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 1bc42a556dbc4..57403aff5d7eb 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -17,7 +17,7 @@ import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; import { HttpService } from '../http_service'; import { Router } from '../router'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; let server: HttpService; let logger: ReturnType; diff --git a/src/core/server/logging/logger.mock.ts b/src/core/server/logging/logger.mock.ts index cfabaeb72adf7..55ce55bc035e4 100644 --- a/src/core/server/logging/logger.mock.ts +++ b/src/core/server/logging/logger.mock.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export { loggerMock } from '@kbn/logging/mocks'; -export type { MockedLogger } from '@kbn/logging/mocks'; +export { loggerMock } from '@kbn/logging-mocks'; +export type { MockedLogger } from '@kbn/logging-mocks'; diff --git a/src/core/server/metrics/collectors/cgroup.test.ts b/src/core/server/metrics/collectors/cgroup.test.ts index 269437f026f2f..3f12107c80ea1 100644 --- a/src/core/server/metrics/collectors/cgroup.test.ts +++ b/src/core/server/metrics/collectors/cgroup.test.ts @@ -7,7 +7,7 @@ */ import mockFs from 'mock-fs'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { OsCgroupMetricsCollector } from './cgroup'; describe('OsCgroupMetricsCollector', () => { diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts index 5592038f1416a..4715fab16cb74 100644 --- a/src/core/server/metrics/collectors/os.test.ts +++ b/src/core/server/metrics/collectors/os.test.ts @@ -8,7 +8,7 @@ jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import os from 'os'; import { cgroupCollectorMock } from './os.test.mocks'; import { OsMetricsCollector } from './os'; diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts index 7d263d8b7d6af..78160729f7bdc 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { mockOsCollector, mockProcessCollector, diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts index 1207594304e64..6ecc09c21ddce 100644 --- a/src/dev/build/tasks/download_cloud_dependencies.ts +++ b/src/dev/build/tasks/download_cloud_dependencies.ts @@ -36,12 +36,19 @@ export const DownloadCloudDependencies: Task = { let buildId = ''; if (!config.isRelease) { - const manifest = await Axios.get( - `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest` - ); - buildId = manifest.data.build.build_id; + const manifestUrl = `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest`; + try { + const manifest = await Axios.get(manifestUrl); + buildId = manifest.data.build.build_id; + } catch (e) { + log.error( + `Unable to find Elastic artifacts for ${config.getBuildVersion()} at ${manifestUrl}.` + ); + throw e; + } } await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat', buildId); await downloadBeat('filebeat', buildId); }, diff --git a/src/dev/code_coverage/nyc_config/nyc.functional.config.js b/src/dev/code_coverage/nyc_config/nyc.functional.config.js index 479c40ec9e109..1b68c23db5f4b 100644 --- a/src/dev/code_coverage/nyc_config/nyc.functional.config.js +++ b/src/dev/code_coverage/nyc_config/nyc.functional.config.js @@ -7,7 +7,18 @@ */ const defaultExclude = require('@istanbuljs/schema/default-exclude'); -const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/{test, types}/**/*']; +const extraExclude = [ + 'data/optimize/**', + '**/{__jest__,__test__,__examples__,__fixtures__,__snapshots__,__stories__,*mock*,*storybook,target,types}/**/*', + '**/{integration_tests,test,tests,test_helpers,test_data,test_samples,test_utils,test_utilities,*scripts}/**/*', + '**/{*e2e*,fixtures,manual_tests,stub*}/**', + '**/*mock*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + '**/types.ts', + '**/*.d.ts', + '**/index.{js,ts,tsx}', +]; const path = require('path'); module.exports = { @@ -16,5 +27,9 @@ module.exports = { : 'target/kibana-coverage/functional', 'report-dir': 'target/kibana-coverage/functional-combined', reporter: ['html', 'json-summary'], + include: [ + 'src/{core,plugins}/**/*.{js,mjs,jsx,ts,tsx}', + 'x-pack/plugins/**/*.{js,mjs,jsx,ts,tsx}', + ], exclude: extraExclude.concat(defaultExclude), }; diff --git a/src/dev/code_coverage/nyc_config/nyc.server.config.js b/src/dev/code_coverage/nyc_config/nyc.server.config.js new file mode 100644 index 0000000000000..d8cebf468d0db --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.server.config.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const path = require('path'); + +module.exports = { + extends: '@istanbuljs/nyc-config-typescript', + 'report-dir': process.env.KIBANA_DIR + ? path.resolve(process.env.KIBANA_DIR, 'target/kibana-coverage/server') + : 'target/kibana-coverage/server', + reporter: ['json'], + all: true, + include: [ + 'src/{core,plugins}/**/*.{js,mjs,jsx,ts,tsx}', + 'x-pack/plugins/**/*.{js,mjs,jsx,ts,tsx}', + ], + exclude: [ + '**/{__jest__,__test__,__examples__,__fixtures__,__snapshots__,__stories__,*mock*,*storybook,target,types}/**/*', + '**/{integration_tests,test,tests,test_helpers,test_data,test_samples,test_utils,test_utilities,*scripts}/**/*', + '**/{*e2e*,fixtures,manual_tests,stub*}/**', + '**/*mock*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + '**/types.ts', + '**/*.d.ts', + '**/index.{js,ts,tsx}', + ], +}; diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap index 1a7ca53228a60..a0b8cddaf4f9c 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap @@ -6,6 +6,7 @@ Object { "type": "render", "value": Object { "args": Object { + "ariaLabel": undefined, "colorMode": "none", "goalAccessor": undefined, "labelMajor": "title", diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts index 2c24aa292319c..61de491595f05 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts @@ -93,14 +93,27 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({ }), required: false, }, + ariaLabel: { + types: ['string'], + help: i18n.translate('expressionGauge.functions.gaugeChart.config.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the gauge chart', + }), + required: false, + }, }, - fn(data, args) { + fn(data, args, handlers) { return { type: 'render', as: EXPRESSION_GAUGE_NAME, value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts index 16f246bf24713..e1cebae438758 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts @@ -46,6 +46,7 @@ export type GaugeArguments = GaugeState & { shape: GaugeShape; colorMode: GaugeColorMode; palette?: PaletteOutput; + ariaLabel?: string; }; export type GaugeInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap index bd39344807643..9af3bb2be8a57 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap @@ -5,6 +5,7 @@ exports[`GaugeComponent renders the chart 1`] = ` renderer="canvas" > diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx index dfd7755c47681..593c18e5e9b05 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx +++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx @@ -231,7 +231,12 @@ export const GaugeComponent: FC = memo( return ( - + ({ defaultMessage: 'The id of the split column or the corresponding dimension', }), }, + ariaLabel: { + types: ['string'], + help: i18n.translate('expressionHeatmap.functions.args.ariaLabelHelpText', { + defaultMessage: 'Specifies the aria label of the heat map', + }), + required: false, + }, }, fn(data, args, handlers) { if (handlers?.inspectorAdapters?.tables) { @@ -203,7 +210,13 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({ as: EXPRESSION_HEATMAP_NAME, value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts index efd4e1a8b990c..10e43e426317d 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts @@ -77,6 +77,7 @@ export interface HeatmapArguments { splitColumnAccessor?: string | ExpressionValueVisDimension; legend: HeatmapLegendConfigResult; gridConfig: HeatmapGridConfigResult; + ariaLabel?: string; } export type HeatmapInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 3c751956c0ea2..c1e026064fdfb 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -521,6 +521,8 @@ export const HeatmapComponent: FC = memo( : NaN, }} onBrushEnd={interactive ? (onBrushEnd as BrushEndListener) : undefined} + ariaLabel={args.ariaLabel} + ariaUseDefaultSummary={!args.ariaLabel} /> = memo( yAxisLabelName={yAxisColumn?.name} xAxisTitle={args.gridConfig.isXAxisTitleVisible ? xAxisTitle : undefined} yAxisTitle={args.gridConfig.isYAxisTitleVisible ? yAxisTitle : undefined} - xAxisLabelFormatter={(v) => `${xValuesFormatter.convert(v) ?? ''}`} + xAxisLabelFormatter={(v) => + args.gridConfig.isXAxisLabelVisible ? `${xValuesFormatter.convert(v)}` : '' + } yAxisLabelFormatter={ yAxisColumn - ? (v) => `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + ? (v) => + args.gridConfig.isYAxisLabelVisible + ? `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + : '' : undefined } /> diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap index de064a44058cc..f1bd7834e52f1 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap @@ -59,6 +59,7 @@ Object { "syncColors": false, "visConfig": Object { "addTooltip": true, + "ariaLabel": undefined, "buckets": Array [ Object { "accessor": 1, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap index 95b8df13882d9..d73f53277a2ba 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -32,6 +32,7 @@ Object { "syncColors": false, "visConfig": Object { "addTooltip": true, + "ariaLabel": undefined, "buckets": Array [ Object { "accessor": 1, @@ -164,6 +165,7 @@ Object { "syncColors": false, "visConfig": Object { "addTooltip": true, + "ariaLabel": undefined, "buckets": Array [ Object { "accessor": 1, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap index d18dca573606a..b8d8032fa5839 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap @@ -59,6 +59,7 @@ Object { "syncColors": false, "visConfig": Object { "addTooltip": true, + "ariaLabel": undefined, "buckets": Array [ Object { "accessor": 1, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap index 54ead941c7548..7c6922cdff84a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -59,6 +59,7 @@ Object { "syncColors": false, "visConfig": Object { "addTooltip": true, + "ariaLabel": undefined, "bucket": Object { "accessor": 1, "format": Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts index 0be470121ecb4..aa433b8eaee2d 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts @@ -93,6 +93,10 @@ export const strings = { i18n.translate('expressionPartitionVis.waffle.function.args.showValuesInLegendHelpText', { defaultMessage: 'Show values in legend', }), + getAriaLabelHelp: () => + i18n.translate('expressionPartitionVis.reusable.functions.args.ariaLabelHelpText', { + defaultMessage: 'Specifies the aria label of the chart', + }), getSliceSizeHelp: () => i18n.translate('expressionPartitionVis.reusable.function.dimension.metric', { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts index 388b0741d23d3..142bc6290d476 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -82,6 +82,11 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ help: strings.getLabelsArgHelp(), default: `{${PARTITION_LABELS_FUNCTION}}`, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -95,6 +100,10 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index c054d572538ce..80302f877698c 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -109,6 +109,11 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ help: strings.getStartFromSecondLargestSliceArgHelp(), default: true, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -117,6 +122,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts index d0ae42b4b7942..65f016729eabe 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -82,6 +82,11 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => help: strings.getLabelsArgHelp(), default: `{${PARTITION_LABELS_FUNCTION}}`, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -95,6 +100,10 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts index ade524aad59c8..b1b30539949c4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -81,6 +81,11 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ help: strings.getShowValuesInLegendArgHelp(), default: false, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -90,6 +95,10 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ const buckets = args.bucket ? [args.bucket] : []; const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 87358d5dbe659..01ca39c9cbb36 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -52,6 +52,7 @@ interface VisCommonParams { legendPosition: Position; truncateLegend: boolean; maxLegendLines: number; + ariaLabel?: string; } interface VisCommonConfig extends VisCommonParams { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap index 4e56d2c5efa4c..b367db1af5437 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -238,6 +238,7 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` > { ]} baseTheme={chartBaseTheme} onRenderChange={onRenderChange} + ariaLabel={props.visParams.ariaLabel} + ariaUseDefaultSummary={!props.visParams.ariaLabel} /> { types: ['vis_dimension'], help: argHelp.bucket, }, + ariaLabel: { + types: ['string'], + help: argHelp.ariaLabel, + required: false, + }, }, fn(input, args, handlers) { const visParams: TagCloudRendererParams = { @@ -136,6 +144,10 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { bucket: args.bucket, }), palette: args.palette, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 091b3e861332d..44fc6f3048790 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -20,6 +20,7 @@ interface TagCloudCommonParams { minFontSize: number; maxFontSize: number; showLabel: boolean; + ariaLabel?: string; } export interface TagCloudVisConfig extends TagCloudCommonParams { diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index 2bec25534f49b..560507f84831a 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -191,7 +191,12 @@ export const TagCloudChart = ({ {(resizeRef) => (
- + { diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 683a1a551f81d..0130d4a5f8118 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -8,6 +8,7 @@ "version": "kibana", "requiredPlugins": [ "data", + "dataViews", "embeddable", "controls", "inspector", diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index ae16527b64440..05d663bdac265 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -110,7 +110,7 @@ export async function mountApp({ uiSettings: coreStart.uiSettings, scopedHistory: () => scopedHistory, screenshotModeService: screenshotMode, - indexPatterns: dataStart.indexPatterns, + dataViews: dataStart.dataViews, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, savedDashboards: dashboardStart.getSavedDashboardLoader(), @@ -212,7 +212,7 @@ export async function mountApp({ .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) ); if (!hasEmbeddableIncoming) { - dataStart.indexPatterns.clearCache(); + dataStart.dataViews.clearCache(); } // dispatch synthetic hash change event to update hash history objects diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 0ef21fca26f29..039a600d153b2 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -24,13 +24,15 @@ import { EmbeddableFactory, ViewMode } from '../../services/embeddable'; import { dashboardStateStore, setDescription, setViewMode } from '../state'; import { DashboardContainerServices } from '../embeddable/dashboard_container'; import { createKbnUrlStateStorage, defer } from '../../../../kibana_utils/public'; -import { Filter, IIndexPattern, IndexPatternsContract } from '../../services/data'; import { useDashboardAppState, UseDashboardStateProps } from './use_dashboard_app_state'; import { getSampleDashboardInput, getSavedDashboardMock, makeDefaultServices, } from '../test_helpers'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; +import type { Filter } from '@kbn/es-query'; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation: () => void; @@ -56,12 +58,10 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); - const indexPatterns = {} as IndexPatternsContract; - const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); - indexPatterns.getDefault = jest - .fn() - .mockImplementation(() => Promise.resolve(defaultIndexPattern)); + const dataViews = {} as DataViewsContract; + const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; + dataViews.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); + dataViews.getDefault = jest.fn().mockImplementation(() => Promise.resolve(defaultDataView)); const data = dataPluginMock.createStartContract(); data.query.filterManager.getUpdates$ = jest.fn().mockImplementation(() => of(void 0)); @@ -71,7 +71,7 @@ const createDashboardAppStateServices = () => { .fn() .mockImplementation(() => of(void 0)); - return { ...defaults, indexPatterns, data }; + return { ...defaults, dataViews, data }; }; const setupEmbeddableFactory = ( diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 8c58eab0ded83..2ce1c87252d38 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -15,6 +15,7 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { DashboardConstants } from '../..'; import { ViewMode } from '../../services/embeddable'; import { useKibana } from '../../services/kibana_react'; +import { DataView } from '../../services/data_views'; import { getNewDashboardTitle } from '../../dashboard_strings'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; @@ -30,7 +31,7 @@ import { tryDestroyDashboardContainer, syncDashboardContainerInput, savedObjectToDashboardState, - syncDashboardIndexPatterns, + syncDashboardDataViews, syncDashboardFilterState, loadSavedDashboardState, buildDashboardContainer, @@ -81,7 +82,7 @@ export const useDashboardAppState = ({ core, chrome, embeddable, - indexPatterns, + dataViews, usageCollection, savedDashboards, initializerContext, @@ -121,7 +122,7 @@ export const useDashboardAppState = ({ search, history, embeddable, - indexPatterns, + dataViews, notifications, kibanaVersion, savedDashboards, @@ -234,11 +235,11 @@ export const useDashboardAppState = ({ /** * Start syncing index patterns between the Query Service and the Dashboard Container. */ - const indexPatternsSubscription = syncDashboardIndexPatterns({ + const dataViewsSubscription = syncDashboardDataViews({ dashboardContainer, - indexPatterns: dashboardBuildContext.indexPatterns, - onUpdateIndexPatterns: (newIndexPatterns) => - setDashboardAppState((s) => ({ ...s, indexPatterns: newIndexPatterns })), + dataViews: dashboardBuildContext.dataViews, + onUpdateDataViews: (newDataViews: DataView[]) => + setDashboardAppState((s) => ({ ...s, dataViews: newDataViews })), }); /** @@ -339,7 +340,7 @@ export const useDashboardAppState = ({ stopWatchingAppStateInUrl(); stopSyncingDashboardFilterState(); lastSavedSubscription.unsubscribe(); - indexPatternsSubscription.unsubscribe(); + dataViewsSubscription.unsubscribe(); tryDestroyDashboardContainer(dashboardContainer); setDashboardAppState((state) => ({ ...state, @@ -368,7 +369,7 @@ export const useDashboardAppState = ({ usageCollection, scopedHistory, notifications, - indexPatterns, + dataViews, kibanaVersion, embeddable, docTitle, diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 1dd39cc3e5ba9..5752a6445d2a9 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -31,7 +31,7 @@ import { } from '../../services/embeddable'; type BuildDashboardContainerProps = DashboardBuildContext & { - data: DashboardAppServices['data']; // the whole data service is required here because it is required by getUrlGeneratorState + data: DashboardAppServices['data']; // the whole data service is required here because it is required by getLocatorParams savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index 90d5a67c3da47..0d1eb3537377f 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; import deepEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; import { DashboardContainer } from '..'; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index 264c8fcb1de2e..729b0d06f4ab8 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -8,7 +8,7 @@ import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter, isFilterPinned } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; import { DashboardContainerInput } from '../..'; import { controlGroupInputIsEqual } from './dashboard_control_group'; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 58f962591b67c..eab3604ff841b 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -18,7 +18,7 @@ export { DashboardSessionStorage } from './dashboard_session_storage'; export { loadSavedDashboardState } from './load_saved_dashboard_state'; export { attemptLoadDashboardByTitle } from './load_dashboard_by_title'; export { syncDashboardFilterState } from './sync_dashboard_filter_state'; -export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns'; +export { syncDashboardDataViews } from './sync_dashboard_data_views'; export { syncDashboardContainerInput } from './sync_dashboard_container_input'; export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 03a03842c0e66..45eda98dcc498 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -28,7 +28,7 @@ export const loadSavedDashboardState = async ({ query, history, notifications, - indexPatterns, + dataViews, savedDashboards, usageCollection, savedDashboardId, @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultDataView(); + await dataViews.ensureDefaultDataView(); try { const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 5a699eb116401..0be2211d4c2fc 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; +import { isFilterPinned } from '@kbn/es-query'; import { convertTimeToUTCString } from '.'; import { NotificationsStart } from '../../services/core'; import { DashboardSavedObject } from '../../saved_dashboards'; @@ -16,7 +17,7 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { dashboardSaveToastStrings } from '../../dashboard_strings'; import { getHasTaggingCapabilitiesGuard } from './dashboard_tagging'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; -import { RefreshInterval, TimefilterContract, esFilters } from '../../services/data'; +import { RefreshInterval, TimefilterContract } from '../../services/data'; import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; import { DashboardSessionStorage } from './dashboard_session_storage'; import { serializeControlGroupToDashboardSavedObject } from './dashboard_control_group'; @@ -81,9 +82,7 @@ export const saveDashboard = async ({ savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; // only save unpinned filters - const unpinnedFilters = savedDashboard - .getFilters() - .filter((filter) => !esFilters.isFilterPinned(filter)); + const unpinnedFilters = savedDashboard.getFilters().filter((filter) => !isFilterPinned(filter)); savedDashboard.searchSource.setField('filter', unpinnedFilters); try { diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts index 0fa7487390cd8..d3930cb5c0621 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts @@ -10,8 +10,9 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { DashboardContainer } from '../embeddable'; -import { esFilters, Filter, Query } from '../../services/data'; +import { Query } from '../../services/data'; import { DashboardConstants, DashboardSavedObject } from '../..'; import { setControlGroupState, @@ -96,13 +97,7 @@ export const applyContainerChangesToState = ({ return; } const { filterManager } = query; - if ( - !esFilters.compareFilters( - input.filters, - filterManager.getFilters(), - esFilters.COMPARE_ALL_OPTIONS - ) - ) { + if (!compareFilters(input.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(input.filters)); applyFilters(latestState.query, input.filters); diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts similarity index 56% rename from src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts rename to src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts index 5460ef7b00037..63cecaa76fb2f 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts @@ -13,48 +13,51 @@ import { distinctUntilChanged, switchMap, filter, mapTo, map } from 'rxjs/operat import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; -import { IndexPattern, IndexPatternsContract } from '../../services/data'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; -interface SyncDashboardIndexPatternsProps { +interface SyncDashboardDataViewsProps { dashboardContainer: DashboardContainer; - indexPatterns: IndexPatternsContract; - onUpdateIndexPatterns: (newIndexPatterns: IndexPattern[]) => void; + dataViews: DataViewsContract; + onUpdateDataViews: (newDataViews: DataView[]) => void; } -export const syncDashboardIndexPatterns = ({ +export const syncDashboardDataViews = ({ dashboardContainer, - indexPatterns, - onUpdateIndexPatterns, -}: SyncDashboardIndexPatternsProps) => { - const updateIndexPatternsOperator = pipe( + dataViews, + onUpdateDataViews, +}: SyncDashboardDataViewsProps) => { + const updateDataViewsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), - map((container: DashboardContainer): IndexPattern[] | undefined => { - let panelIndexPatterns: IndexPattern[] = []; + map((container: DashboardContainer): DataView[] | undefined => { + let panelDataViews: DataView[] = []; Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); if (isErrorEmbeddable(embeddableInstance)) return; - const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; - if (!embeddableIndexPatterns) return; - panelIndexPatterns.push(...embeddableIndexPatterns); + const embeddableDataViews = ( + embeddableInstance.getOutput() as { indexPatterns: DataView[] } + ).indexPatterns; + if (!embeddableDataViews) return; + panelDataViews.push(...embeddableDataViews); }); if (container.controlGroup) { - panelIndexPatterns.push(...(container.controlGroup.getOutput().dataViews ?? [])); + panelDataViews.push(...(container.controlGroup.getOutput().dataViews ?? [])); } - panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + panelDataViews = uniqBy(panelDataViews, 'id'); /** * If no index patterns have been returned yet, and there is at least one embeddable which * hasn't yet loaded, defer the loading of the default index pattern by returning undefined. */ if ( - panelIndexPatterns.length === 0 && + panelDataViews.length === 0 && Object.keys(container.getOutput().embeddableLoaded).length > 0 && Object.values(container.getOutput().embeddableLoaded).some((value) => value === false) ) { return; } - return panelIndexPatterns; + return panelDataViews; }), distinctUntilChanged((a, b) => deepEqual( @@ -63,17 +66,17 @@ export const syncDashboardIndexPatterns = ({ ) ), // using switchMap for previous task cancellation - switchMap((panelIndexPatterns?: IndexPattern[]) => { + switchMap((panelDataViews?: DataView[]) => { return new Observable((observer) => { - if (!panelIndexPatterns) return; - if (panelIndexPatterns.length > 0) { + if (!panelDataViews) return; + if (panelDataViews.length > 0) { if (observer.closed) return; - onUpdateIndexPatterns(panelIndexPatterns); + onUpdateDataViews(panelDataViews); observer.complete(); } else { - indexPatterns.getDefault().then((defaultIndexPattern) => { + dataViews.getDefault().then((defaultDataView) => { if (observer.closed) return; - onUpdateIndexPatterns([defaultIndexPattern as IndexPattern]); + onUpdateDataViews([defaultDataView as DataView]); observer.complete(); }); } @@ -81,11 +84,11 @@ export const syncDashboardIndexPatterns = ({ }) ); - const indexPatternSources = [dashboardContainer.getOutput$()]; + const dataViewSources = [dashboardContainer.getOutput$()]; if (dashboardContainer.controlGroup) - indexPatternSources.push(dashboardContainer.controlGroup.getOutput$()); + dataViewSources.push(dashboardContainer.controlGroup.getOutput$()); - return combineLatest(indexPatternSources) - .pipe(mapTo(dashboardContainer), updateIndexPatternsOperator) + return combineLatest(dataViewSources) + .pipe(mapTo(dashboardContainer), updateDataViewsOperator) .subscribe(); }; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index 36b8b57cfdbd8..ce9535e549446 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -8,10 +8,10 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; -import { esFilters } from '../../../../data/public'; import { createHashHistory } from 'history'; +import { FilterStateStore } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../dashboard_constants'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; @@ -118,7 +118,7 @@ describe('when global filters change', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts index 2f19924d45982..8af3f2a10666f 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -9,8 +9,11 @@ import { ApplicationStart } from 'kibana/public'; import { QueryState } from '../../../../data/public'; import { setStateToKbnUrl } from '../../../../kibana_utils/public'; -import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { + DashboardConstants, + createDashboardEditUrl, + GLOBAL_STATE_STORAGE_KEY, +} from '../../dashboard_constants'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; export const getDashboardListItemLink = ( diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts index 616fe56102df9..656f5672e38c0 100644 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts @@ -13,7 +13,7 @@ import { UrlForwardingStart } from '../../../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../services/navigation'; import { DashboardAppServices, DashboardAppCapabilities } from '../../types'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; -import { IndexPatternsContract, SavedQueryService } from '../../services/data'; +import { DataViewsContract, SavedQueryService } from '../../services/data'; import { savedObjectsPluginMock } from '../../../../saved_objects/public/mocks'; import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; @@ -83,7 +83,7 @@ export function makeDefaultServices(): DashboardAppServices { savedObjectsClient: core.savedObjects.client, dashboardCapabilities: defaultCapabilities, data: dataPluginMock.createStartContract(), - indexPatterns: {} as IndexPatternsContract, + dataViews: {} as DataViewsContract, savedQueryService: {} as SavedQueryService, scopedHistory: () => ({} as ScopedHistory), setHeaderActionMenu: (mountPoint) => {}, diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 005d40a90f38f..eb251ad41f62b 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -525,7 +525,7 @@ export function DashboardTopNav({ showDatePicker, showFilterBar, setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, - indexPatterns: dashboardAppState.indexPatterns, + indexPatterns: dashboardAppState.dataViews, showSaveQuery: dashboardCapabilities.saveQuery, useDefaultBehaviors: true, savedQuery: state.savedQuery, diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 9063b279c25f2..88fbc3b30392f 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -9,6 +9,7 @@ import type { ControlStyle } from '../../controls/public'; export const DASHBOARD_STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DashboardConstants = { LANDING_PAGE_PATH: '/list', diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index f25a92275d723..bff2d4d79108c 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -16,15 +16,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export type { - DashboardSetup, - DashboardStart, - DashboardUrlGenerator, - DashboardFeatureFlagConfig, -} from './plugin'; - -export type { DashboardUrlGeneratorState } from './url_generator'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; export type { DashboardSavedObject } from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index f3f5aec9f478c..11ec16908b811 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -9,7 +9,7 @@ import { DashboardAppLocatorDefinition } from './locator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { FilterStateStore } from '@kbn/es-query'; describe('dashboard locator', () => { beforeEach(() => { @@ -79,7 +79,7 @@ describe('dashboard locator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index b6655e246de36..42efb521cf6e5 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -8,11 +8,11 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { flow } from 'lodash'; -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 type { SavedDashboardPanel } from '../common/types'; import type { RawDashboardState } from './types'; -import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; @@ -152,12 +152,14 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition( '_g', cleanEmptyKeys({ time: params.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), + filters: filters?.filter((f) => isFilterPinned(f)), refreshInterval: params.refreshInterval, }), { useHash }, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 7f784d43c0cb7..2f63062ccf60c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,8 +33,8 @@ import { UiActionsSetup, UiActionsStart } from './services/ui_actions'; import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; -import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from './services/data'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; +import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; +import { SharePluginSetup, SharePluginStart } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { ScreenshotModePluginSetup, @@ -70,29 +70,15 @@ import { CopyToDashboardAction, DashboardCapabilities, } from './application'; -import { - createDashboardUrlGenerator, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from './url_generator'; import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; import { SpacesPluginStart } from './services/spaces'; -declare module '../../share/public' { - export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export type DashboardUrlGenerator = UrlGeneratorContract; - export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } @@ -134,15 +120,6 @@ export interface DashboardStart { getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; - /** - * @deprecated Use dashboard locator instead. Dashboard locator is available - * under `.locator` key. This dashboard URL generator will be removed soon. - * - * ```ts - * plugins.dashboard.locator.getLocation({ ... }); - * ``` - */ - dashboardUrlGenerator?: DashboardUrlGenerator; locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -157,11 +134,6 @@ export class DashboardPlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; - - /** - * @deprecated Use locator instead. - */ - private dashboardUrlGenerator?: DashboardUrlGenerator; private locator?: DashboardAppLocator; public setup( @@ -178,20 +150,6 @@ export class DashboardPlugin ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); - const startServices = core.getStartServices(); - - if (share) { - this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( - createDashboardUrlGenerator(async () => { - const [coreStart, , selfStart] = await startServices; - return { - appBasePath: coreStart.application.getUrlForApp('dashboards'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - savedDashboardLoader: selfStart.getSavedDashboardLoader(), - }; - }) - ); - } const getPlaceholderEmbeddableStartServices = async () => { const [coreStart] = await core.getStartServices(); @@ -253,10 +211,13 @@ export class DashboardPlugin filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) + map(async ({ state }) => { + const { isFilterPinned } = await import('@kbn/es-query'); + return { + ...state, + filters: state.filters?.filter(isFilterPinned), + }; + }) ), }, ], @@ -455,7 +416,6 @@ export class DashboardPlugin factory: dashboardContainerFactory as DashboardContainerFactory, }); }, - dashboardUrlGenerator: this.dashboardUrlGenerator, locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; diff --git a/src/plugins/dashboard/public/services/data_views.ts b/src/plugins/dashboard/public/services/data_views.ts new file mode 100644 index 0000000000000..4fb2bbaf08503 --- /dev/null +++ b/src/plugins/dashboard/public/services/data_views.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '../../../data_views/public'; diff --git a/src/plugins/dashboard/public/services/share.ts b/src/plugins/dashboard/public/services/share.ts index 7ed9b86571596..77a9f44a3cf00 100644 --- a/src/plugins/dashboard/public/services/share.ts +++ b/src/plugins/dashboard/public/services/share.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export type { - SharePluginStart, - SharePluginSetup, - UrlGeneratorContract, -} from '../../../share/public'; +export type { SharePluginStart, SharePluginSetup } from '../../../share/public'; export { downloadMultipleAs } from '../../../share/public'; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index b7b146aeba348..4de07974203a7 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -17,22 +17,25 @@ import type { KibanaExecutionContext, } from 'kibana/public'; import { History } from 'history'; +import type { Filter } from '@kbn/es-query'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; -import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; -import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; + +import { DataView } from './services/data_views'; import { SharePluginStart } from './services/share'; import { EmbeddableStart } from './services/embeddable'; import { DashboardSessionStorage } from './application/lib'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { UsageCollectionSetup } from './services/usage_collection'; import { NavigationPublicPluginStart } from './services/navigation'; +import { Query, RefreshInterval, TimeRange } from './services/data'; import { DashboardPanelState, SavedDashboardPanel } from '../common/types'; import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; -import { DataPublicPluginStart, IndexPatternsContract } from './services/data'; +import { DataPublicPluginStart, DataViewsContract } from './services/data'; +import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects'; -import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { ScreenshotModePluginStart } from './services/screenshot_mode'; +import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; @@ -102,7 +105,7 @@ export interface DashboardContainerInput extends ContainerInput { */ export interface DashboardAppState { hasUnsavedChanges?: boolean; - indexPatterns?: IndexPattern[]; + dataViews?: DataView[]; updateLastSavedState?: () => void; resetToLastSavedState?: () => void; savedDashboard?: DashboardSavedObject; @@ -119,7 +122,7 @@ export interface DashboardAppState { export type DashboardBuildContext = Pick< DashboardAppServices, | 'embeddable' - | 'indexPatterns' + | 'dataViews' | 'savedDashboards' | 'usageCollection' | 'initializerContext' @@ -198,7 +201,7 @@ export interface DashboardAppServices { savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; visualizations: VisualizationsStart; - indexPatterns: IndexPatternsContract; + dataViews: DataViewsContract; usageCollection?: UsageCollectionSetup; navigation: NavigationPublicPluginStart; dashboardCapabilities: DashboardAppCapabilities; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts deleted file mode 100644 index 9a1204f116c7f..0000000000000 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createDashboardUrlGenerator } from './url_generator'; -import { hashedItemStore } from '../../kibana_utils/public'; -import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters, Filter } from '../../data/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; - -const APP_BASE_PATH: string = 'xyz/app/dashboards'; - -const createMockDashboardLoader = ( - dashboardToFilters: { - [dashboardId: string]: () => Filter[]; - } = {} -) => { - return { - get: async (dashboardId: string) => { - return { - searchSource: { - getField: (field: string) => { - if (field === 'filter') - return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; - throw new Error( - `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` - ); - }, - }, - }; - }, - } as SavedObjectLoader; -}; - -describe('dashboard url generator', () => { - beforeEach(() => { - // @ts-ignore - hashedItemStore.storage = mockStorage; - }); - - test('creates a link to a saved dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/create?_a=()&_g=()"`); - }); - - test('creates a link with global time range set up', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - }, - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - }, - ], - query: { query: 'bye', language: 'kuery' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('searchSessionId', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [], - query: { query: 'bye', language: 'kuery' }, - searchSessionId: '__sessionSearchId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` - ); - }); - - test('savedQuery', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - savedQuery: '__savedQueryId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` - ); - expect(url).toContain('__savedQueryId__'); - }); - - test('panels', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - panels: [{ fakePanelContent: 'fakePanelContent' } as any], - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` - ); - }); - - test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a false useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: true, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a true useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: false, - }); - expect(url.indexOf('relative')).toBeGreaterThan(1); - }); - - describe('preserving saved filters', () => { - const savedFilter1 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter1' }, - }; - - const savedFilter2 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter2' }, - }; - - const appliedFilter = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'appliedfilter' }, - }; - - test('attaches filters from destination dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - ['dashboard2']: () => [savedFilter2], - }), - }) - ); - - const urlToDashboard1 = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); - expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); - - const urlToDashboard2 = await generator.createUrl!({ - dashboardId: 'dashboard2', - filters: [appliedFilter], - }); - - expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); - expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => { - throw new Error('Not found'); - }, - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test('can enforce empty filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [], - preserveSavedFilters: false, - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); - }); - - test('no filters in result url if no filters applied', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - }); - expect(url).not.toEqual(expect.stringContaining('filters')); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/view/dashboard1?_a=()&_g=()"`); - }); - - test('can turn off preserving filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - preserveSavedFilters: false, - }); - - expect(urlWithPreservedFiltersTurnedOff).not.toEqual( - expect.stringContaining('query:savedfilter1') - ); - expect(urlWithPreservedFiltersTurnedOff).toEqual( - expect.stringContaining('query:appliedfilter') - ); - }); - }); -}); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts deleted file mode 100644 index 5c0cd32ee5a16..0000000000000 --- a/src/plugins/dashboard/public/url_generator.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; -import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { ViewMode } from '../../embeddable/public'; -import { DashboardConstants } from './dashboard_constants'; -import { SavedDashboardPanel } from '../common/types'; - -export const STATE_STORAGE_KEY = '_a'; -export const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; - -/** - * @deprecated Use dashboard locator instead. - */ -export interface DashboardUrlGeneratorState { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * View mode of the dashboard. - */ - viewMode?: ViewMode; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: SavedDashboardPanel[]; - - /** - * Saved query ID - */ - savedQuery?: string; -} - -/** - * @deprecated Use dashboard locator instead. - */ -export const createDashboardUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - savedDashboardLoader: SavedObjectLoader; - }> -): UrlGeneratorsDefinition => ({ - id: DASHBOARD_APP_URL_GENERATOR, - createUrl: async (state) => { - const startServices = await getStartServices(); - const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - const hash = state.dashboardId ? `view/${state.dashboardId}` : `create`; - - const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (state.preserveSavedFilters === false) return []; - if (!state.dashboardId) return []; - try { - const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; - } catch (e) { - // in case dashboard is missing, built the url without those filters - // dashboard app will handle redirect to landing page with toast message - return []; - } - }; - - const cleanEmptyKeys = (stateObj: Record) => { - Object.keys(stateObj).forEach((key) => { - if (stateObj[key] === undefined) { - delete stateObj[key]; - } - }); - return stateObj; - }; - - // leave filters `undefined` if no filters was applied - // in this case dashboard will restore saved filters on its own - const filters = state.filters && [ - ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), - ...state.filters, - ]; - - let url = setStateToKbnUrl( - STATE_STORAGE_KEY, - cleanEmptyKeys({ - query: state.query, - filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), - viewMode: state.viewMode, - panels: state.panels, - savedQuery: state.savedQuery, - }), - { useHash }, - `${appBasePath}#/${hash}` - ); - - url = setStateToKbnUrl( - GLOBAL_STATE_STORAGE_KEY, - cleanEmptyKeys({ - time: state.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), - refreshInterval: state.refreshInterval, - }), - { useHash }, - url - ); - - if (state.searchSessionId) { - url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; - } - - return url; - }, -}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index e0cd410ce5e8f..ed8f87ad9b51b 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -25,7 +25,7 @@ import { convertSavedDashboardPanelToPanelState, } from '../../common/embeddable/embeddable_saved_object_converters'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; import { mergeMigrationFunctionMaps, MigrateFunction, @@ -49,7 +49,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 8980bd1903323..4000bed0c28ac 100644 --- a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { esFilters, Filter } from 'src/plugins/data/public'; +import { FilterStateStore, Filter } from '@kbn/es-query'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, query: {}, - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, }; const queryFilter: Pre600FilterQuery = { @@ -27,7 +27,7 @@ test('Migrates an old filter query into the query field', () => { expect(newSearchSource).toEqual({ filter: [ { - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, meta: { alias: '', disabled: false, diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts index ddd1c45841b9c..e2ea076de7743 100644 --- a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts @@ -7,14 +7,14 @@ */ import type { SavedObjectMigrationFn } from 'kibana/server'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ ...doc, references: Array.isArray(doc.references) ? doc.references.map((reference) => { if (reference.type === 'index_pattern') { - reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + reference.type = DATA_VIEW_SAVED_OBJECT_TYPE; } return reference; }) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 680d06780543a..55049447aee57 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../controls/tsconfig.json" }, diff --git a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx index 063f60b82927e..c3a02a8abe8e0 100644 --- a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx @@ -57,6 +57,7 @@ interface TimelionVisComponentProps { seriesList: Sheet; onBrushEvent: (rangeFilterParams: RangeFilterParams) => void; renderComplete: IInterpreterRenderHandlers['done']; + ariaLabel?: string; } const DefaultYAxis = () => ( @@ -98,6 +99,7 @@ export const TimelionVisComponent = ({ seriesList, renderComplete, onBrushEvent, + ariaLabel, }: TimelionVisComponentProps) => { const kibana = useKibana(); const chartRef = useRef(null); @@ -206,6 +208,8 @@ export const TimelionVisComponent = ({ type: TooltipType.VerticalCursor, }} externalPointerEvents={{ tooltip: { visible: false } }} + ariaLabel={ariaLabel} + ariaUseDefaultSummary={!ariaLabel} /> ) => void; + defaultIndexPattern?: IndexPattern; } -export const newAnnotation = () => ({ +export const newAnnotation = (defaultIndexPattern?: IndexPattern) => () => ({ id: uuid.v1(), color: '#F00', - index_pattern: '', + index_pattern: + defaultIndexPattern && defaultIndexPattern.id ? { id: defaultIndexPattern.id } : '', time_field: '', icon: 'fa-tag', ignore_global_filters: 1, @@ -60,7 +63,11 @@ export const AnnotationsEditor = (props: AnnotationsEditorProps) => { const { annotations } = props.model; const handleAdd = useCallback( - () => collectionActions.handleAdd(getCollectionActionsProps(props), newAnnotation), + () => + collectionActions.handleAdd( + getCollectionActionsProps(props), + newAnnotation(props.defaultIndexPattern) + ), [props] ); diff --git a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js index aa5333f2facfe..217b3948e1cd8 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js @@ -69,6 +69,7 @@ export const IndexPattern = ({ model: _model, allowLevelOfDetail, allowIndexSwitchingMode, + baseIndexPattern, }) => { const config = getUISettings(); const timeFieldName = `${prefix}time_field`; @@ -148,9 +149,11 @@ export const IndexPattern = ({ indexPatternString: undefined, }; + const indexPatternToFetch = index || baseIndexPattern; + try { - fetchedIndexPattern = index - ? await fetchIndexPattern(index, indexPatterns, { + fetchedIndexPattern = indexPatternToFetch + ? await fetchIndexPattern(indexPatternToFetch, indexPatterns, { fetchKibanaIndexForStringIndexes: true, }) : { @@ -165,7 +168,7 @@ export const IndexPattern = ({ } fetchIndex(); - }, [index]); + }, [index, baseIndexPattern]); const toggleIndicatorDisplay = useCallback( () => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }), @@ -386,6 +389,7 @@ IndexPattern.defaultProps = { }; IndexPattern.propTypes = { + baseIndexPattern: PropTypes.oneOf([PropTypes.object, PropTypes.string]), model: PropTypes.object.isRequired, fields: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx index 7c2a0b4527a3e..a613595622b01 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx @@ -166,6 +166,7 @@ export class TimeseriesPanelConfig extends Component< fields={this.props.fields} model={this.props.model} onChange={this.props.onChange} + defaultIndexPattern={this.props.defaultIndexPattern} /> ); } else { diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts b/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts index 653235a672825..ecbc5af601be7 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'; import { IUiSettingsClient } from 'kibana/public'; +import type { IndexPattern } from 'src/plugins/data/public'; import type { TimeseriesVisData } from '../../../../common/types'; import { TimeseriesVisParams } from '../../../types'; import { VisFields } from '../../lib/fetch_fields'; @@ -18,6 +19,7 @@ export interface PanelConfigProps { visData$: Observable; getConfig: IUiSettingsClient['get']; onChange: (partialModel: Partial) => void; + defaultIndexPattern?: IndexPattern; } export enum PANEL_CONFIG_TABS { diff --git a/src/plugins/vis_types/timeseries/public/application/components/series_config.js b/src/plugins/vis_types/timeseries/public/application/components/series_config.js index 7c40bfcaa8c24..53d2fb994c919 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/series_config.js @@ -43,6 +43,19 @@ export const SeriesConfig = (props) => { ); const isKibanaIndexPattern = props.panel.use_kibana_indexes || seriesIndexPattern === ''; + const { indexPatternForQuery, onChange } = props; + const onChangeOverride = useCallback( + (partialState) => { + const stateUpdate = { ...partialState }; + const isEnabling = partialState.override_index_pattern; + if (isEnabling && !model.series_index_pattern) { + stateUpdate.series_index_pattern = indexPatternForQuery; + } + onChange(stateUpdate); + }, + [model.series_index_pattern, indexPatternForQuery, onChange] + ); + return (
@@ -126,12 +139,13 @@ export const SeriesConfig = (props) => { ; + defaultIndexPattern?: IndexPattern; } interface TimeseriesEditorState { @@ -216,6 +218,7 @@ export class VisEditor extends Component
diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js index 87e2efd818af6..2ce95093e2e3d 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js @@ -338,6 +338,19 @@ export const TimeseriesConfig = injectI18n(function (props) { ); const isKibanaIndexPattern = props.panel.use_kibana_indexes || seriesIndexPattern === ''; + const { indexPatternForQuery, onChange } = props; + const onChangeOverride = useCallback( + (partialState) => { + const stateUpdate = { ...partialState }; + const isEnabling = partialState.override_index_pattern; + if (isEnabling && !model.series_index_pattern) { + stateUpdate.series_index_pattern = indexPatternForQuery; + } + onChange(stateUpdate); + }, + [model.series_index_pattern, indexPatternForQuery, onChange] + ); + const initialPalette = model.palette ?? { type: 'palette', name: 'default', @@ -545,7 +558,7 @@ export const TimeseriesConfig = injectI18n(function (props) { @@ -556,6 +569,7 @@ export const TimeseriesConfig = injectI18n(function (props) { prefix="series_" disabled={!model.override_index_pattern} allowLevelOfDetail={true} + baseIndexPattern={indexPatternForQuery} /> diff --git a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx index 001762b0f0d5b..bdf265b37b26c 100644 --- a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx +++ b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx @@ -15,7 +15,7 @@ import type { IEditorController, EditorRenderProps, } from 'src/plugins/visualizations/public'; -import { getUISettings, getI18n, getCoreStart } from '../services'; +import { getUISettings, getI18n, getCoreStart, getDataStart } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; import type { TimeseriesVisParams } from '../types'; import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -30,8 +30,9 @@ export class EditorController implements IEditorController { private embeddableHandler: VisualizeEmbeddableContract ) {} - render({ timeRange, uiState, filters, query }: EditorRenderProps) { + async render({ timeRange, uiState, filters, query }: EditorRenderProps) { const I18nContext = getI18n().Context; + const defaultIndexPattern = (await getDataStart().dataViews.getDefault()) || undefined; render( @@ -45,6 +46,7 @@ export class EditorController implements IEditorController { uiState={uiState} filters={filters} query={query} + defaultIndexPattern={defaultIndexPattern} /> , diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.test.ts b/src/plugins/vis_types/timeseries/public/metrics_type.test.ts index 3d75a36cca424..f9eda5a18b79d 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.test.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.test.ts @@ -20,11 +20,6 @@ describe('metricsVisDefinition', () => { let defaultParams: TimeseriesVisParams; beforeEach(async () => { - defaultParams = ( - await metricsVisDefinition.setup!({ - params: cloneDeep(metricsVisDefinition.visConfig.defaults), - } as unknown as Vis) - ).params; setDataStart({ indexPatterns: { async getDefault() { @@ -42,6 +37,11 @@ describe('metricsVisDefinition', () => { }, } as unknown as DataViewsContract, } as DataPublicPluginStart); + defaultParams = ( + await metricsVisDefinition.setup!({ + params: cloneDeep(metricsVisDefinition.visConfig.defaults), + } as unknown as Vis) + ).params; }); it('should resolve correctly the base index pattern by id', async () => { diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 548368b30759a..5cb77efc5bcba 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -52,6 +52,19 @@ export const withReplacedIds = ( return vis; }; +async function withDefaultIndexPattern( + vis: Vis +): Promise> { + const { indexPatterns } = getDataStart(); + + const defaultIndex = await indexPatterns.getDefault(); + if (!defaultIndex || !defaultIndex.id) return vis; + vis.params.index_pattern = { + id: defaultIndex.id, + }; + return vis; +} + async function resolveIndexPattern( indexPatternValue: IndexPatternValue, indexPatterns: DataViewsContract @@ -138,7 +151,7 @@ export const metricsVisDefinition: VisTypeDefinition< drop_last_bucket: 0, }, }, - setup: (vis) => Promise.resolve(withReplacedIds(vis)), + setup: (vis) => withDefaultIndexPattern(withReplacedIds(vis)), editorConfig: { editor: TSVB_EDITOR_NAME, }, diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts index 7c17f003dfbab..af6eb44affabc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { AUTO_INTERVAL } from '../../../common/constants'; import { validateField } from '../../../common/fields_utils'; import { validateInterval } from '../../../common/validate_interval'; +import { TimeFieldNotSpecifiedError } from '../../../common/errors'; import type { FetchedIndexPattern, Panel, Series } from '../../../common/types'; @@ -34,7 +35,11 @@ export function getIntervalAndTimefield( } if (panel.use_kibana_indexes) { - validateField(timeField!, index); + if (timeField) { + validateField(timeField, index); + } else { + throw new TimeFieldNotSpecifiedError(); + } } let interval = panel.interval; diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx index 1393fc252fcf6..26b9d5ee6edf7 100644 --- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx @@ -62,6 +62,7 @@ type XYSettingsProps = Pick< legendPosition: Position; truncateLegend: boolean; maxLegendLines: number; + ariaLabel?: string; }; function getValueLabelsStyling() { @@ -96,6 +97,7 @@ export const XYSettings: FC = ({ legendPosition, maxLegendLines, truncateLegend, + ariaLabel, }) => { const themeService = getThemeService(); const theme = themeService.useChartsTheme(); @@ -173,6 +175,8 @@ export const XYSettings: FC = ({ onRenderChange={onRenderChange} legendAction={legendAction} tooltip={tooltipProps} + ariaLabel={ariaLabel} + ariaUseDefaultSummary={!ariaLabel} orderOrdinalBinsBy={ orderBucketsBySum ? { diff --git a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts index ccad0c520f8ea..28908901d71a6 100644 --- a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts +++ b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts @@ -232,10 +232,21 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ }), multi: true, }, + ariaLabel: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the xy chart', + }), + required: false, + }, }, fn(context, args, handlers) { const visType = args.chartType; const visConfig = { + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, type: args.chartType, addLegend: args.addLegend, addTooltip: args.addTooltip, diff --git a/src/plugins/vis_types/xy/public/types/param.ts b/src/plugins/vis_types/xy/public/types/param.ts index 81eeca55108ca..943705deb3726 100644 --- a/src/plugins/vis_types/xy/public/types/param.ts +++ b/src/plugins/vis_types/xy/public/types/param.ts @@ -145,6 +145,7 @@ export interface VisParams { palette: PaletteOutput; fillOpacity?: number; fittingFunction?: Exclude; + ariaLabel?: string; } export interface XYVisConfig { @@ -185,4 +186,5 @@ export interface XYVisConfig { seriesDimension?: ExpressionValueXYDimension[]; splitRowDimension?: ExpressionValueXYDimension[]; splitColumnDimension?: ExpressionValueXYDimension[]; + ariaLabel?: string; } diff --git a/src/plugins/vis_types/xy/public/vis_component.tsx b/src/plugins/vis_types/xy/public/vis_component.tsx index 8574e86a23096..c4e438d54dfb5 100644 --- a/src/plugins/vis_types/xy/public/vis_component.tsx +++ b/src/plugins/vis_types/xy/public/vis_component.tsx @@ -373,6 +373,7 @@ const VisComponent = (props: VisComponentProps) => { splitSeriesAccessors, splitChartColumnAccessor ?? splitChartRowAccessor )} + ariaLabel={visParams.ariaLabel} onBrushEnd={handleBrush(visData, xAccessor, 'interval' in config.aspects.x.params)} onRenderChange={onRenderChange} legendAction={ diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index c3b8834605d1d..a12195e34a81e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -97,6 +97,7 @@ export class VisualizeEmbeddable private filters?: Filter[]; private searchSessionId?: string; private syncColors?: boolean; + private embeddableTitle?: string; private visCustomizations?: Pick; private subscriptions: Subscription[] = []; private expression?: ExpressionAstExpression; @@ -140,6 +141,7 @@ export class VisualizeEmbeddable this.syncColors = this.input.syncColors; this.searchSessionId = this.input.searchSessionId; this.query = this.input.query; + this.embeddableTitle = this.getTitle(); this.vis = vis; this.vis.uiState.on('change', this.uiStateChangeHandler); @@ -259,6 +261,11 @@ export class VisualizeEmbeddable dirty = true; } + if (this.embeddableTitle !== this.getTitle()) { + this.embeddableTitle = this.getTitle(); + dirty = true; + } + if (this.vis.description && this.domNode) { this.domNode.setAttribute('data-description', this.vis.description); } @@ -406,6 +413,9 @@ export class VisualizeEmbeddable query: this.input.query, filters: this.input.filters, }, + variables: { + embeddableTitle: this.getTitle(), + }, searchSessionId: this.input.searchSessionId, syncColors: this.input.syncColors, uiState: this.vis.uiState, diff --git a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts index 2faa66d258eb6..059f172e51b46 100644 --- a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts +++ b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts @@ -33,7 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { return bools.every((currBool) => currBool === true); }; - describe('saved objects inspect page', () => { + // FLAKY: https://github.com/elastic/kibana/issues/118288 + describe.skip('saved objects inspect page', () => { beforeEach(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 6bbd5ea72015d..9e6888a319c38 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index b1e76880dc912..775839764b410 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index ce352d1f63c28..70c7ea6d7827b 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 90cc06d2088c8..dc251faaee827 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 7850768e9466c..89df8d2f4146b 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 19b76ac66efcf..7bd4ff7dedfa0 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index dc89a473a38ab..1896db426da5f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -65,8 +65,8 @@ export const alertType: RuleType< range(instances) .map(() => uuid.v4()) .forEach((id: string) => { - services - .alertInstanceFactory(id) + services.alertFactory + .create(id) .replaceState({ triggerdOnCycle: count }) .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index c5d4af6872c83..a29b280a34fff 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -70,7 +70,7 @@ export const alertType: RuleType< if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) { peopleInCraft.forEach(({ craft, name }) => { - services.alertInstanceFactory(name).replaceState({ craft }).scheduleActions('default'); + services.alertFactory.create(name).replaceState({ craft }).scheduleActions('default'); }); } diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index f838832b6ea66..3c5c459d5a780 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -556,6 +556,8 @@ When creating a new action type, your plugin will eventually call `server.plugin Consider working with the alerting team on early structure /design feedback of new actions, especially as the APIs and infrastructure are still under development. +Don't forget to ping @elastic/security-detections-response to see if the new connector should be enabled within their solution. + ## licensing Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 90c02b6e1c254..bc917fbf43bc4 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -40,8 +40,6 @@ Table of Contents > References to `rule` and `rule type` entities are still named `AlertType` within the codebase. -> References to `alert` and `alert factory` entities are still named `AlertInstance` and `alertInstanceFactory` within the codebase. - **Rule Type**: A function that takes parameters and executes actions on alerts. **Rule**: A configuration that defines a schedule, a rule type w/ parameters, state information and actions. @@ -113,7 +111,7 @@ This is the primary function for a rule type. Whenever the rule needs to execute |---|---| |services.scopedClusterClient|This is an instance of the Elasticsearch client. Use this to do Elasticsearch queries in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to perform CRUD operations on any saved object that lives in the same space as the rule.

The scope of the saved objects client is tied to the user who created the rule (only when security is enabled).| -|services.alertInstanceFactory(id)|This [alert factory](#alert-factory) creates alerts and must be used in order to execute actions. The id you give to the alert factory is a unique identifier for the alert.| +|services.alertFactory|This [alert factory](#alert-factory) creates alerts and must be used in order to schedule action execution. The id you give to the alert factory create function() is a unique identifier for the alert.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |services.shouldWriteAlerts()|This returns a boolean indicating whether the executor should write out alerts as data. This is determined by whether rule execution has been cancelled due to timeout AND whether both the Kibana `cancelAlertsOnRuleTimeout` flag and the rule type `cancelAlertsOnRuleTimeout` are set to `true`.| |services.shouldStopExecution()|This returns a boolean indicating whether rule execution has been cancelled due to timeout.| @@ -310,7 +308,7 @@ const myRuleType: RuleType< // scenario the provided server will be used. Also, this ID will be // used to make `getState()` return previous state, if any, on // matching identifiers. - const alert = services.alertInstanceFactory(server); + const alert = services.alertFactory.create(server); // State from the last execution. This will exist if an alert was // created and executed in the previous execution @@ -731,13 +729,13 @@ Query: ## Alert Factory -**alertInstanceFactory(id)** +**alertFactory.create(id)** -One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to execute actions. The `id` you give to the alert factory is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. +One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to schedule action execution. The `id` you give to the alert factory create fn() is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. Note that the `id` only needs to be unique **within the scope of a specific rule**, not unique across all rules or rule types. For example, Rule 1 and Rule 2 can both create an alert with an `id` of `"a"` without conflicting with one another. But if Rule 1 creates 2 alerts, then they must be differentiated with `id`s of `"a"` and `"b"`. -This factory returns an instance of `AlertInstance`. The `AlertInstance` class has the following methods. Note that we have removed the methods that you shouldn't touch. +This factory returns an instance of `Alert`. The `Alert` class has the following methods. Note that we have removed the methods that you shouldn't touch. |Method|Description| |---|---| @@ -781,7 +779,8 @@ The templating engine is [mustache]. General definition for the [mustache variab The following code would be within a rule type. As you can see `cpuUsage` will replace the state of the alert and `server` is the context for the alert to execute. The difference between the two is that `cpuUsage` will be accessible at the next execution. ``` -alertInstanceFactory('server_1') +alertFactory + .create('server_1') .replaceState({ cpuUsage: 80, }) diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts new file mode 100644 index 0000000000000..83b82de904703 --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -0,0 +1,488 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import sinon from 'sinon'; +import { Alert } from './alert'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; + +let clock: sinon.SinonFakeTimers; + +beforeAll(() => { + clock = sinon.useFakeTimers(); +}); +beforeEach(() => clock.reset()); +afterAll(() => clock.restore()); + +describe('hasScheduledActions()', () => { + test('defaults to false', () => { + const alert = new Alert(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('returns true when scheduleActions is called', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + }); +}); + +describe('isThrottled', () => { + test(`should throttle when group didn't change and throttle period is still active`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test(`shouldn't throttle when group didn't change and throttle period expired`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('15s')).toEqual(false); + }); + + test(`shouldn't throttle when group changes`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(5000); + alert.scheduleActions('other-group'); + expect(alert.isThrottled('1m')).toEqual(false); + }); +}); + +describe('scheduledActionGroupOrSubgroupHasChanged()', () => { + test('should be false if no last scheduled and nothing scheduled', () => { + const alert = new Alert(); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group and subgroup does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from undefined to defined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from defined to undefined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be true if no last scheduled and has scheduled action', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('penguin'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('penguin', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does not change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); +}); + +describe('getScheduledActionOptions()', () => { + test('defaults to undefined', () => { + const alert = new Alert(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('unscheduleActions()', () => { + test('makes hasScheduledActions() return false', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + alert.unscheduleActions(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('makes getScheduledActionOptions() return undefined', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: {}, + state: {}, + }); + alert.unscheduleActions(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('getState()', () => { + test('returns state passed to constructor', () => { + const state = { foo: true }; + const alert = new Alert({ + state, + }); + expect(alert.getState()).toEqual(state); + }); +}); + +describe('scheduleActions()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActions('default', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('scheduleActionsWithSubGroup()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and subgroup is the same', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return false when throttled and subgroup is the different', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'prev-subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + subgroup: 'subgroup', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice with different subgroups', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice whether there are subgroups', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('replaceState()', () => { + test('replaces previous state', () => { + const alert = new Alert({ + state: { foo: true }, + }); + alert.replaceState({ bar: true }); + expect(alert.getState()).toEqual({ bar: true }); + alert.replaceState({ baz: true }); + expect(alert.getState()).toEqual({ baz: true }); + }); +}); + +describe('updateLastScheduledActions()', () => { + test('replaces previous lastScheduledActions', () => { + const alert = new Alert({ + meta: {}, + }); + alert.updateLastScheduledActions('default'); + expect(alert.toJSON()).toEqual({ + state: {}, + meta: { + lastScheduledActions: { + date: new Date().toISOString(), + group: 'default', + }, + }, + }); + }); +}); + +describe('toJSON', () => { + test('only serializes state and meta', () => { + const alertInstance = new Alert( + { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + } + ); + expect(JSON.stringify(alertInstance)).toEqual( + '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' + ); + }); +}); + +describe('toRaw', () => { + test('returns unserialised underlying state and meta', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }; + const alertInstance = new Alert( + raw + ); + expect(alertInstance.toRaw()).toEqual(raw); + }); +}); diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerting/server/alert/alert.ts similarity index 97% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.ts rename to x-pack/plugins/alerting/server/alert/alert.ts index b41a4e551040c..d34aa68ac1a11 100644 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -27,16 +27,16 @@ interface ScheduledExecutionOptions< state: State; } -export type PublicAlertInstance< +export type PublicAlert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + Alert, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; -export class AlertInstance< +export class Alert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts similarity index 61% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts rename to x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index 6518305dcd109..ecb1a10bbac42 100644 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -6,8 +6,8 @@ */ import sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { createAlertInstanceFactory } from './create_alert_instance_factory'; +import { Alert } from './alert'; +import { createAlertFactory } from './create_alert_factory'; let clock: sinon.SinonFakeTimers; @@ -17,9 +17,9 @@ beforeAll(() => { beforeEach(() => clock.reset()); afterAll(() => clock.restore()); -test('creates new instances for ones not passed in', () => { - const alertInstanceFactory = createAlertInstanceFactory({}); - const result = alertInstanceFactory('1'); +test('creates new alerts for ones not passed in', () => { + const alertFactory = createAlertFactory({ alerts: {} }); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object {}, @@ -28,15 +28,17 @@ test('creates new instances for ones not passed in', () => { `); }); -test('reuses existing instances', () => { - const alertInstance = new AlertInstance({ +test('reuses existing alerts', () => { + const alert = new Alert({ state: { foo: true }, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); - const alertInstanceFactory = createAlertInstanceFactory({ - '1': alertInstance, + const alertFactory = createAlertFactory({ + alerts: { + '1': alert, + }, }); - const result = alertInstanceFactory('1'); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object { @@ -52,11 +54,11 @@ test('reuses existing instances', () => { `); }); -test('mutates given instances', () => { - const alertInstances = {}; - const alertInstanceFactory = createAlertInstanceFactory(alertInstances); - alertInstanceFactory('1'); - expect(alertInstances).toMatchInlineSnapshot(` +test('mutates given alerts', () => { + const alerts = {}; + const alertFactory = createAlertFactory({ alerts }); + alertFactory.create('1'); + expect(alerts).toMatchInlineSnapshot(` Object { "1": Object { "meta": Object {}, diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts new file mode 100644 index 0000000000000..07f4dbc7b20ea --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.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 { AlertInstanceContext, AlertInstanceState } from '../types'; +import { Alert } from './alert'; + +export interface CreateAlertFactoryOpts< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +> { + alerts: Record>; +} + +export function createAlertFactory< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>({ alerts }: CreateAlertFactoryOpts) { + return { + create: (id: string): Alert => { + if (!alerts[id]) { + alerts[id] = new Alert(); + } + + return alerts[id]; + }, + }; +} diff --git a/x-pack/plugins/alerting/server/alert_instance/index.ts b/x-pack/plugins/alerting/server/alert/index.ts similarity index 57% rename from x-pack/plugins/alerting/server/alert_instance/index.ts rename to x-pack/plugins/alerting/server/alert/index.ts index 7b5dd064c5dca..5e1a9ee626b57 100644 --- a/x-pack/plugins/alerting/server/alert_instance/index.ts +++ b/x-pack/plugins/alerting/server/alert/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export type { PublicAlertInstance } from './alert_instance'; -export { AlertInstance } from './alert_instance'; -export { createAlertInstanceFactory } from './create_alert_instance_factory'; +export type { PublicAlert } from './alert'; +export { Alert } from './alert'; +export { createAlertFactory } from './create_alert_factory'; diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts deleted file mode 100644 index 68fed6aa7d3fd..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts +++ /dev/null @@ -1,604 +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 sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; - -let clock: sinon.SinonFakeTimers; - -beforeAll(() => { - clock = sinon.useFakeTimers(); -}); -beforeEach(() => clock.reset()); -afterAll(() => clock.restore()); - -describe('hasScheduledActions()', () => { - test('defaults to false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); -}); - -describe('isThrottled', () => { - test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('15s')).toEqual(false); - }); - - test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(5000); - alertInstance.scheduleActions('other-group'); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); -}); - -describe('scheduledActionGroupOrSubgroupHasChanged()', () => { - test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('penguin'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('penguin', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); -}); - -describe('getScheduledActionOptions()', () => { - test('defaults to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('unscheduleActions()', () => { - test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - alertInstance.unscheduleActions(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: {}, - state: {}, - }); - alertInstance.unscheduleActions(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('getState()', () => { - test('returns state passed to constructor', () => { - const state = { foo: true }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state }); - expect(alertInstance.getState()).toEqual(state); - }); -}); - -describe('scheduleActions()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActions('default', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('scheduleActionsWithSubGroup()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'prev-subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - subgroup: 'subgroup', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('replaceState()', () => { - test('replaces previous state', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true } }); - alertInstance.replaceState({ bar: true }); - expect(alertInstance.getState()).toEqual({ bar: true }); - alertInstance.replaceState({ baz: true }); - expect(alertInstance.getState()).toEqual({ baz: true }); - }); -}); - -describe('updateLastScheduledActions()', () => { - test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ meta: {} }); - alertInstance.updateLastScheduledActions('default'); - expect(alertInstance.toJSON()).toEqual({ - state: {}, - meta: { - lastScheduledActions: { - date: new Date().toISOString(), - group: 'default', - }, - }, - }); - }); -}); - -describe('toJSON', () => { - test('only serializes state and meta', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - expect(JSON.stringify(alertInstance)).toEqual( - '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' - ); - }); -}); - -describe('toRaw', () => { - test('returns unserialised underlying state and meta', () => { - const raw = { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(raw); - expect(alertInstance.toRaw()).toEqual(raw); - }); -}); diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts deleted file mode 100644 index 2faaff157fd82..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AlertInstanceContext, AlertInstanceState } from '../types'; -import { AlertInstance } from './alert_instance'; - -export function createAlertInstanceFactory< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext, - ActionGroupIds extends string ->(alertInstances: Record>) { - return (id: string): AlertInstance => { - if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); - } - - return alertInstances[id]; - }; -} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 36ee1f8ee9676..63e8df5488895 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -32,7 +32,7 @@ export type { export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; export type { PluginSetupContract, PluginStartContract } from './plugin'; export type { FindResult } from './rules_client'; -export type { PublicAlertInstance as AlertInstance } from './alert_instance'; +export type { PublicAlert as Alert } from './alert'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; export type { diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index c4702f796ad8e..afbc3ef9cec43 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -7,7 +7,7 @@ import { rulesClientMock } from './rules_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { AlertInstance } from './alert_instance'; +import { Alert } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, @@ -37,30 +37,33 @@ const createStartMock = () => { export type AlertInstanceMock< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext -> = jest.Mocked>; -const createAlertInstanceFactoryMock = < - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext ->() => { - const mock = { - hasScheduledActions: jest.fn(), - isThrottled: jest.fn(), - getScheduledActionOptions: jest.fn(), - unscheduleActions: jest.fn(), - getState: jest.fn(), - scheduleActions: jest.fn(), - replaceState: jest.fn(), - updateLastScheduledActions: jest.fn(), - toJSON: jest.fn(), - toRaw: jest.fn(), - }; +> = jest.Mocked>; + +const createAlertFactoryMock = { + create: < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext + >() => { + const mock = { + hasScheduledActions: jest.fn(), + isThrottled: jest.fn(), + getScheduledActionOptions: jest.fn(), + unscheduleActions: jest.fn(), + getState: jest.fn(), + scheduleActions: jest.fn(), + replaceState: jest.fn(), + updateLastScheduledActions: jest.fn(), + toJSON: jest.fn(), + toRaw: jest.fn(), + }; - // support chaining - mock.replaceState.mockReturnValue(mock); - mock.unscheduleActions.mockReturnValue(mock); - mock.scheduleActions.mockReturnValue(mock); + // support chaining + mock.replaceState.mockReturnValue(mock); + mock.unscheduleActions.mockReturnValue(mock); + mock.scheduleActions.mockReturnValue(mock); - return mock as unknown as AlertInstanceMock; + return mock as unknown as AlertInstanceMock; + }, }; const createAbortableSearchClientMock = () => { @@ -82,11 +85,11 @@ const createAlertServicesMock = < InstanceState extends AlertInstanceState = AlertInstanceState, InstanceContext extends AlertInstanceContext = AlertInstanceContext >() => { - const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); + const alertFactoryMockCreate = createAlertFactoryMock.create(); return { - alertInstanceFactory: jest - .fn>, [string]>() - .mockReturnValue(alertInstanceFactoryMock), + alertFactory: { + create: jest.fn().mockReturnValue(alertFactoryMockCreate), + }, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => true, @@ -97,7 +100,7 @@ const createAlertServicesMock = < export type AlertServicesMock = ReturnType; export const alertsMock = { - createAlertInstanceFactory: createAlertInstanceFactoryMock, + createAlertFactory: createAlertFactoryMock, createSetup: createSetupMock, createStart: createStartMock, createAlertServices: createAlertServicesMock, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 9100772a806e8..63e35583bc9a1 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -22,23 +22,23 @@ import { import { esKuery } from '../../../../../src/plugins/data/server'; import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { - Alert, - PartialAlert, + Alert as Rule, + PartialAlert as PartialRule, RawRule, RuleTypeRegistry, - AlertAction, + AlertAction as RuleAction, IntervalSchedule, - SanitizedAlert, + SanitizedAlert as SanitizedRule, RuleTaskState, AlertSummary, - AlertExecutionStatusValues, - AlertNotifyWhenType, - AlertTypeParams, + AlertExecutionStatusValues as RuleExecutionStatusValues, + AlertNotifyWhenType as RuleNotifyWhenType, + AlertTypeParams as RuleTypeParams, ResolvedSanitizedRule, - AlertWithLegacyId, + AlertWithLegacyId as RuleWithLegacyId, SanitizedRuleWithLegacyId, - PartialAlertWithLegacyId, - RawAlertInstance, + PartialAlertWithLegacyId as PartialRuleWithLegacyId, + RawAlertInstance as RawAlert, } from '../types'; import { validateRuleTypeParams, ruleExecutionStatusFromRaw, getAlertNotifyWhenType } from '../lib'; import { @@ -74,7 +74,7 @@ import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField, validateOperationOnAttributes } from './lib'; import { getRuleExecutionStatusPending } from '../lib/rule_execution_status'; -import { AlertInstance } from '../alert_instance'; +import { Alert } from '../alert'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; @@ -82,7 +82,7 @@ import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; } -type NormalizedAlertAction = Omit; +type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = | { apiKeysEnabled: false } | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; @@ -174,16 +174,16 @@ export interface AggregateResult { ruleMutedStatus?: { muted: number; unmuted: number }; } -export interface FindResult { +export interface FindResult { page: number; perPage: number; total: number; - data: Array>; + data: Array>; } -export interface CreateOptions { +export interface CreateOptions { data: Omit< - Alert, + Rule, | 'id' | 'createdBy' | 'updatedBy' @@ -202,7 +202,7 @@ export interface CreateOptions { }; } -export interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -211,7 +211,7 @@ export interface UpdateOptions { actions: NormalizedAlertAction[]; params: Params; throttle: string | null; - notifyWhen: AlertNotifyWhenType | null; + notifyWhen: RuleNotifyWhenType | null; }; } @@ -248,7 +248,7 @@ export class RulesClient { private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; private readonly auditLogger?: AuditLogger; private readonly eventLogger?: IEventLogger; - private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; + private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; constructor({ ruleTypeRegistry, @@ -286,10 +286,10 @@ export class RulesClient { this.eventLogger = eventLogger; } - public async create({ + public async create({ data, options, - }: CreateOptions): Promise> { + }: CreateOptions): Promise> { const id = options?.id || SavedObjectsUtils.generateId(); try { @@ -432,7 +432,7 @@ export class RulesClient { ); } - public async get({ + public async get({ id, includeLegacyId = false, excludeFromPublicApi = false, @@ -440,7 +440,7 @@ export class RulesClient { id: string; includeLegacyId?: boolean; excludeFromPublicApi?: boolean; - }): Promise | SanitizedRuleWithLegacyId> { + }): Promise | SanitizedRuleWithLegacyId> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { await this.authorization.ensureAuthorized({ @@ -475,7 +475,7 @@ export class RulesClient { ); } - public async resolve({ + public async resolve({ id, includeLegacyId, }: { @@ -612,7 +612,7 @@ export class RulesClient { }); } - public async find({ + public async find({ options: { fields, ...options } = {}, excludeFromPublicApi = false, }: { options?: FindOptions; excludeFromPublicApi?: boolean } = {}): Promise> { @@ -762,7 +762,7 @@ export class RulesClient { }, }; - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { placeholder.alertExecutionStatus[key] = 0; } @@ -783,7 +783,7 @@ export class RulesClient { }; // Fill missing keys with zeroes - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { if (!ret.alertExecutionStatus.hasOwnProperty(key)) { ret.alertExecutionStatus[key] = 0; } @@ -878,10 +878,10 @@ export class RulesClient { return removeResult; } - public async update({ + public async update({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { return await retryIfConflicts( this.logger, `rulesClient.update('${id}')`, @@ -889,10 +889,10 @@ export class RulesClient { ); } - private async updateWithOCC({ + private async updateWithOCC({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { let alertSavedObject: SavedObject; try { @@ -974,10 +974,10 @@ export class RulesClient { return updateResult; } - private async updateAlert( + private async updateAlert( { id, data }: UpdateOptions, { attributes, version }: SavedObject - ): Promise> { + ): Promise> { const ruleType = this.ruleTypeRegistry.get(attributes.alertTypeId); // Validate @@ -1048,7 +1048,7 @@ export class RulesClient { throw e; } - return this.getPartialAlertFromRaw( + return this.getPartialRuleFromRaw( id, ruleType, updatedObject.attributes, @@ -1332,12 +1332,12 @@ export class RulesClient { try { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(attributes.scheduledTaskId), - attributes as unknown as SanitizedAlert + attributes as unknown as SanitizedRule ); - const recoveredAlertInstances = mapValues, AlertInstance>( + const recoveredAlertInstances = mapValues, Alert>( state.alertInstances ?? {}, - (rawAlertInstance) => new AlertInstance(rawAlertInstance) + (rawAlertInstance) => new Alert(rawAlertInstance) ); const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); @@ -1568,7 +1568,7 @@ export class RulesClient { } private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1636,7 +1636,7 @@ export class RulesClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1751,22 +1751,22 @@ export class RulesClient { ...omit(action, 'actionRef'), id: reference.id, }; - }) as Alert['actions']; + }) as Rule['actions']; } - private getAlertFromRaw( + private getAlertFromRaw( id: string, ruleTypeId: string, rawRule: RawRule, references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): Alert | AlertWithLegacyId { + ): Rule | RuleWithLegacyId { const ruleType = this.ruleTypeRegistry.get(ruleTypeId); // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawRule, it is safe // to cast the result to an Alert - const res = this.getPartialAlertFromRaw( + const res = this.getPartialRuleFromRaw( id, ruleType, rawRule, @@ -1776,13 +1776,13 @@ export class RulesClient { ); // include to result because it is for internal rules client usage if (includeLegacyId) { - return res as AlertWithLegacyId; + return res as RuleWithLegacyId; } // exclude from result because it is an internal variable - return omit(res, ['legacyId']) as Alert; + return omit(res, ['legacyId']) as Rule; } - private getPartialAlertFromRaw( + private getPartialRuleFromRaw( id: string, ruleType: UntypedNormalizedRuleType, { @@ -1801,7 +1801,7 @@ export class RulesClient { references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): PartialAlert | PartialAlertWithLegacyId { + ): PartialRule | PartialRuleWithLegacyId { const rule = { id, notifyWhen, @@ -1820,8 +1820,8 @@ export class RulesClient { }; return includeLegacyId - ? ({ ...rule, legacyId } as PartialAlertWithLegacyId) - : (rule as PartialAlert); + ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) + : (rule as PartialRule); } private async validateActions( @@ -1873,8 +1873,8 @@ export class RulesClient { } private async extractReferences< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleType: UntypedNormalizedRuleType, ruleActions: NormalizedAlertAction[], @@ -1909,8 +1909,8 @@ export class RulesClient { } private injectReferencesIntoParams< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleId: string, ruleType: UntypedNormalizedRuleType, diff --git a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts index 8322e42b0743c..1337c5a70979b 100644 --- a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; import { TaskRunnerFactory } from '../task_runner'; import { RuleTypeRegistry, ConstructorOptions } from '../rule_type_registry'; import { taskManagerMock } from '../../../task_manager/server/mocks'; 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 402cc3951d39b..b5a98af23d74b 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 @@ -309,7 +309,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); @@ -427,8 +427,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subDefault'); } ); @@ -708,7 +708,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -934,8 +934,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -991,7 +991,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1192,7 +1192,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1268,8 +1268,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subgroup1'); } ); @@ -1350,7 +1350,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1672,7 +1672,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2080,10 +2080,10 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); // create an instance, but don't schedule any actions, so it doesn't go active - executorServices.alertInstanceFactory('3'); + executorServices.alertFactory.create('3'); } ); const taskRunner = new TaskRunner( @@ -2186,7 +2186,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2297,7 +2297,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const date = new Date().toISOString(); @@ -3692,8 +3692,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4006,8 +4006,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4251,8 +4251,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -5035,7 +5035,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); 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 785a68e1a24b9..9b77ec7f8dc72 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -15,7 +15,7 @@ import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; -import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; +import { Alert as CreatedAlert, createAlertFactory } from '../alert'; import { validateRuleTypeParams, executionStatusFromState, @@ -285,7 +285,7 @@ export class TaskRunner< async executeAlert( alertId: string, - alert: AlertInstance, + alert: CreatedAlert, executionHandler: ExecutionHandler ) { const { @@ -333,8 +333,8 @@ export class TaskRunner< const alerts = mapValues< Record, - AlertInstance - >(alertRawInstances, (rawAlert) => new AlertInstance(rawAlert)); + CreatedAlert + >(alertRawInstances, (rawAlert) => new CreatedAlert(rawAlert)); const originalAlerts = cloneDeep(alerts); const originalAlertIds = new Set(Object.keys(originalAlerts)); @@ -358,11 +358,13 @@ export class TaskRunner< executionId: this.executionId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory< + alertFactory: createAlertFactory< InstanceState, InstanceContext, WithoutReservedActionGroups - >(alerts), + >({ + alerts, + }), shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), shouldStopExecution: () => this.cancelled, search: createAbortableEsClientFactory({ @@ -420,11 +422,11 @@ export class TaskRunner< // Cleanup alerts that are no longer scheduling actions to avoid over populating the alertInstances object const alertsWithScheduledActions = pickBy( alerts, - (alert: AlertInstance) => alert.hasScheduledActions() + (alert: CreatedAlert) => alert.hasScheduledActions() ); const recoveredAlerts = pickBy( alerts, - (alert: AlertInstance, id) => + (alert: CreatedAlert, id) => !alert.hasScheduledActions() && originalAlertIds.has(id) ); @@ -478,7 +480,7 @@ export class TaskRunner< const alertsToExecute = notifyWhen === 'onActionGroupChange' ? Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const shouldExecuteAction = alert.scheduledActionGroupOrSubgroupHasChanged(); if (!shouldExecuteAction) { this.logger.debug( @@ -489,7 +491,7 @@ export class TaskRunner< } ) : Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const throttled = alert.isThrottled(throttle); const muted = mutedAlertIdsSet.has(alertName); const shouldExecuteAction = !throttled && !muted; @@ -506,7 +508,7 @@ export class TaskRunner< const allTriggeredActions = await Promise.all( alertsToExecute.map( - ([alertId, alert]: [string, AlertInstance]) => + ([alertId, alert]: [string, CreatedAlert]) => this.executeAlert(alertId, alert, executionHandler) ) ); @@ -533,7 +535,7 @@ export class TaskRunner< triggeredActions, alertTypeState: updatedRuleTypeState || undefined, alertInstances: mapValues< - Record>, + Record>, RawAlertInstance >(alertsWithScheduledActions, (alert) => alert.toRaw()), }; @@ -910,9 +912,9 @@ interface TrackAlertDurationsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext > { - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; } function trackAlertDurations< @@ -967,9 +969,9 @@ interface GenerateNewAndRecoveredAlertEventsParams< > { eventLogger: IEventLogger; executionId: string; - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleId: string; ruleLabel: string; namespace: string | undefined; @@ -1117,7 +1119,7 @@ interface ScheduleActionsForRecoveredAlertsParams< > { logger: Logger; recoveryActionGroup: ActionGroup; - recoveredAlerts: Dictionary>; + recoveredAlerts: Dictionary>; executionHandler: ExecutionHandler; mutedAlertIdsSet: Set; ruleLabel: string; @@ -1173,8 +1175,8 @@ interface LogActiveAndRecoveredAlertsParams< RecoveryActionGroupId extends string > { logger: Logger; - activeAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + activeAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleLabel: string; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 47f888fc71136..f4b67935f7249 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -359,7 +359,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout to false here @@ -393,7 +393,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout for ruleType to false here @@ -427,7 +427,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index de6649bb44891..9d6302774f889 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,7 +7,7 @@ import type { IRouter, RequestHandlerContext, SavedObjectReference } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PublicAlertInstance } from './alert_instance'; +import { PublicAlert } from './alert'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -74,9 +74,9 @@ export interface AlertServices< InstanceContext extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: ( - id: string - ) => PublicAlertInstance; + alertFactory: { + create: (id: string) => PublicAlert; + }; shouldWriteAlerts: () => boolean; shouldStopExecution: () => boolean; search: IAbortableClusterClient; diff --git a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts index 2d98c09096f5e..01aa64b85f720 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts @@ -39,7 +39,7 @@ describe('Error count alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts with service name and environment for those that exceeded the threshold', async () => { @@ -138,7 +138,7 @@ describe('Error count alert', () => { 'apm.error_rate_foo_env-foo-2', 'apm.error_rate_bar_env-bar', ].forEach((instanceName) => - expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) ); expect(scheduleActions).toHaveBeenCalledTimes(3); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index 889fe3c16596e..41bb5126646fc 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -32,7 +32,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('ml jobs are not available', async () => { @@ -59,7 +59,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('anomaly is less than threshold', async () => { @@ -110,7 +110,7 @@ describe('Transaction duration anomaly alert', () => { expect( services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); }); @@ -183,9 +183,9 @@ describe('Transaction duration anomaly alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_duration_anomaly_foo_development_type-foo' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts index b0a99377c2989..64540e144d8a8 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts @@ -46,7 +46,7 @@ describe('Transaction error rate alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts for services that exceeded the threshold', async () => { @@ -117,12 +117,12 @@ describe('Transaction error rate alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_error_rate_foo_type-foo_env-foo' ); - expect(services.alertInstanceFactory).not.toHaveBeenCalledWith( + expect(services.alertFactory.create).not.toHaveBeenCalledWith( 'apm.transaction_error_rate_bar_type-bar_env-bar' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index a8610bbcc8d37..f881b4476fe22 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -42,7 +42,7 @@ export const createRuleTypeMocks = () => { savedObjectsClient: { get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), }, - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, alertWithLifecycle: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.tsx b/x-pack/plugins/canvas/public/expression_types/datasource.tsx index 5f85b5ec20abd..a7d5cd4e7a2b4 100644 --- a/x-pack/plugins/canvas/public/expression_types/datasource.tsx +++ b/x-pack/plugins/canvas/public/expression_types/datasource.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useRef, useCallback, ReactPortal, useState, memo } from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; +import usePrevious from 'react-use/lib/usePrevious'; import deepEqual from 'react-fast-compare'; import { Ast } from '@kbn/interpreter'; import { createPortal } from 'react-dom'; @@ -58,6 +59,7 @@ const DatasourceWrapperComponent: React.FunctionComponent(); const { spec, datasourceProps, handlers } = props; + const prevSpec = usePrevious(spec); const onMount = useCallback((ref) => { datasourceRef.current = ref ?? undefined; @@ -83,15 +85,25 @@ const DatasourceWrapperComponent: React.FunctionComponent { + if (argument && prevSpec?.name !== spec?.name) { + setArgument(undefined); + datasourceRef.current = undefined; + } + }, [argument, prevSpec?.name, spec?.name]); + useEffect(() => { if (datasourceRef.current) { datasourceRef.current.updateProps(datasourceProps); } }, [datasourceProps]); - useEffectOnce(() => () => { + useEffectOnce(() => { datasourceRef.current = undefined; - handlers.destroy(); + return () => { + datasourceRef.current = undefined; + handlers.destroy(); + }; }); return ( diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 3cbcee62d8c09..da43d6864f156 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -116,7 +116,7 @@ export class CasesClientFactory { caseConfigureService: new CaseConfigureService(this.logger), connectorMappingsService: new ConnectorMappingsService(this.logger), userActionService: new CaseUserActionService(this.logger), - attachmentService: new AttachmentService(this.logger), + attachmentService, logger: this.logger, lensEmbeddableFactory: this.options.lensEmbeddableFactory, authorization: auth, diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 5ed2d2978f154..821f0a8aaa2f9 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -25,7 +25,7 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; import { CasesService } from '.'; diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 2b30e4d4de628..94df7c21a82a9 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -22,7 +22,7 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CaseConfigureService } from '.'; import { ESCasesConfigureAttributes } from './types'; import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index eb1e69a3014ab..084f319cbb1e0 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -6,7 +6,7 @@ */ import { get } from 'lodash'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { SavedObject, SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss new file mode 100644 index 0000000000000..09abf97829be4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss @@ -0,0 +1,4 @@ +.crawlSelectDomainsModal { + width: 50rem; + max-width: 90%; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx new file mode 100644 index 0000000000000..79898d9f15e9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiModal, EuiModalFooter, EuiButton, EuiButtonEmpty } from '@elastic/eui'; + +import { rerender } from '../../../../../test_helpers'; + +import { CrawlSelectDomainsModal } from './crawl_select_domains_modal'; +import { SimplifiedSelectable } from './simplified_selectable'; + +const MOCK_VALUES = { + // CrawlerLogic + domains: [{ url: 'https://www.elastic.co' }, { url: 'https://www.swiftype.com' }], + // CrawlSelectDomainsModalLogic + selectedDomainUrls: ['https://www.elastic.co'], + isModalVisible: true, +}; + +const MOCK_ACTIONS = { + // CrawlSelectDomainsModalLogic + hideModal: jest.fn(), + onSelectDomainUrls: jest.fn(), + // CrawlerLogic + startCrawl: jest.fn(), +}; + +describe('CrawlSelectDomainsModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + + wrapper = shallow(); + }); + + it('is empty when the modal is hidden', () => { + setMockValues({ + ...MOCK_VALUES, + isModalVisible: false, + }); + + rerender(wrapper); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders as a modal when visible', () => { + expect(wrapper.is(EuiModal)).toBe(true); + }); + + it('can be closed', () => { + expect(wrapper.prop('onClose')).toEqual(MOCK_ACTIONS.hideModal); + expect(wrapper.find(EuiModalFooter).find(EuiButtonEmpty).prop('onClick')).toEqual( + MOCK_ACTIONS.hideModal + ); + }); + + it('allows the user to select domains', () => { + expect(wrapper.find(SimplifiedSelectable).props()).toEqual({ + options: ['https://www.elastic.co', 'https://www.swiftype.com'], + selectedOptions: ['https://www.elastic.co'], + onChange: MOCK_ACTIONS.onSelectDomainUrls, + }); + }); + + describe('submit button', () => { + it('is disabled when no domains are selected', () => { + setMockValues({ + ...MOCK_VALUES, + selectedDomainUrls: [], + }); + + rerender(wrapper); + + expect(wrapper.find(EuiModalFooter).find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('starts a crawl and hides the modal', () => { + wrapper.find(EuiModalFooter).find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: MOCK_VALUES.selectedDomainUrls, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx new file mode 100644 index 0000000000000..211266a779df9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; +import { SimplifiedSelectable } from './simplified_selectable'; + +import './crawl_select_domains_modal.scss'; + +export const CrawlSelectDomainsModal: React.FC = () => { + const { domains } = useValues(CrawlerLogic); + const domainUrls = domains.map((domain) => domain.url); + + const crawlSelectDomainsModalLogic = CrawlSelectDomainsModalLogic({ domains }); + const { isDataLoading, isModalVisible, selectedDomainUrls } = useValues( + crawlSelectDomainsModalLogic + ); + const { hideModal, onSelectDomainUrls } = useActions(crawlSelectDomainsModalLogic); + + const { startCrawl } = useActions(CrawlerLogic); + + if (!isModalVisible) { + return null; + } + + return ( + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.modalHeaderTitle', + { + defaultMessage: 'Crawl select domains', + } + )} + + + 0 ? 'accent' : 'subdued'} + > + {selectedDomainUrls.length} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.selectedDescriptor', + { + defaultMessage: 'selected', + } + )} + + + + + + + + {CANCEL_BUTTON_LABEL} + { + startCrawl({ domain_allowlist: selectedDomainUrls }); + }} + disabled={selectedDomainUrls.length === 0} + isLoading={isDataLoading} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.startCrawlButtonLabel', + { + defaultMessage: 'Apply and crawl now', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts new file mode 100644 index 0000000000000..ef6ef4d09fadb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; + +describe('CrawlSelectDomainsModalLogic', () => { + const { mount } = new LogicMounter(CrawlSelectDomainsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlSelectDomainsModalLogic.values).toEqual({ + isDataLoading: false, + isModalVisible: false, + selectedDomainUrls: [], + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + CrawlSelectDomainsModalLogic.actions.hideModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + + describe('showModal', () => { + it('shows the modal', () => { + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(true); + }); + + it('resets the selected options', () => { + mount({ + selectedDomainUrls: ['https://www.elastic.co', 'https://www.swiftype.com'], + }); + + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([]); + }); + }); + + describe('onSelectDomainUrls', () => { + it('saves the urls', () => { + mount({ + selectedDomainUrls: [], + }); + + CrawlSelectDomainsModalLogic.actions.onSelectDomainUrls([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + }); + }); + + describe('[CrawlerLogic.actionTypes.startCrawl]', () => { + it('enables loading state', () => { + mount({ + isDataLoading: false, + }); + + CrawlerLogic.actions.startCrawl(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(true); + }); + }); + + describe('[CrawlerLogic.actionTypes.onStartCrawlRequestComplete]', () => { + it('disables loading state and hides the modal', () => { + mount({ + isDataLoading: true, + isModalVisible: true, + }); + + CrawlerLogic.actions.onStartCrawlRequestComplete(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(false); + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts new file mode 100644 index 0000000000000..088950cbffd3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlerDomain } from '../../types'; + +export interface CrawlSelectDomainsLogicProps { + domains: CrawlerDomain[]; +} + +export interface CrawlSelectDomainsLogicValues { + isDataLoading: boolean; + isModalVisible: boolean; + selectedDomainUrls: string[]; +} + +export interface CrawlSelectDomainsModalLogicActions { + hideModal(): void; + onSelectDomainUrls(domainUrls: string[]): { domainUrls: string[] }; + showModal(): void; +} + +export const CrawlSelectDomainsModalLogic = kea< + MakeLogicType< + CrawlSelectDomainsLogicValues, + CrawlSelectDomainsModalLogicActions, + CrawlSelectDomainsLogicProps + > +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawl_select_domains_modal'], + actions: () => ({ + hideModal: true, + onSelectDomainUrls: (domainUrls) => ({ domainUrls }), + showModal: true, + }), + reducers: () => ({ + isDataLoading: [ + false, + { + [CrawlerLogic.actionTypes.startCrawl]: () => true, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + selectedDomainUrls: [ + [], + { + showModal: () => [], + onSelectDomainUrls: (_, { domainUrls }) => domainUrls, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx new file mode 100644 index 0000000000000..a90259f8dac3c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelectable, EuiSelectableList, EuiSelectableSearch } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { SimplifiedSelectable } from './simplified_selectable'; + +describe('SimplifiedSelectable', () => { + let wrapper: ShallowWrapper; + + const MOCK_ON_CHANGE = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('combines the options and selected options', () => { + expect(wrapper.find(EuiSelectable).prop('options')).toEqual([ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + }); + + it('passes newly selected options to the callback', () => { + wrapper.find(EuiSelectable).simulate('change', [ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + + expect(MOCK_ON_CHANGE).toHaveBeenCalledWith(['cat', 'fish']); + }); + + describe('select all button', () => { + it('it is disabled when all options are already selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="SelectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="SelectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith(['cat', 'dog', 'fish']); + }); + }); + + describe('deselect all button', () => { + it('it is disabled when all no options are selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="DeselectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="DeselectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith([]); + }); + }); + + it('renders a search bar and selectable list', () => { + const fullRender = mountWithIntl( + + ); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx new file mode 100644 index 0000000000000..07ede1c59971a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSelectable } from '@elastic/eui'; +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + options: string[]; + selectedOptions: string[]; + onChange(selectedOptions: string[]): void; +} + +export interface OptionMap { + [key: string]: boolean; +} + +export const SimplifiedSelectable: React.FC = ({ options, selectedOptions, onChange }) => { + const selectedOptionsMap: OptionMap = selectedOptions.reduce( + (acc, selectedOption) => ({ + ...acc, + [selectedOption]: true, + }), + {} + ); + + const selectableOptions: Array> = options.map((option) => ({ + label: option, + checked: selectedOptionsMap[option] ? 'on' : undefined, + })); + + return ( + <> + + + onChange(options)} + disabled={selectedOptions.length === options.length} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.selectAllButtonLabel', + { + defaultMessage: 'Select all', + } + )} + + + + onChange([])} + disabled={selectedOptions.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.deselectAllButtonLabel', + { + defaultMessage: 'Deselect all', + } + )} + + + + { + onChange( + newSelectableOptions.filter((option) => option.checked).map((option) => option.label) + ); + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index c46c360934d0b..cc8b1891838b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -16,6 +16,7 @@ import { EuiButton } from '@elastic/eui'; import { CrawlerDomain, CrawlerStatus } from '../../types'; import { CrawlerStatusIndicator } from './crawler_status_indicator'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; const MOCK_VALUES = { @@ -72,9 +73,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Start a crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); @@ -87,9 +86,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Retry crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c02e45f02c407..d750cf100202f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerLogic); + const { stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', { @@ -40,26 +41,27 @@ export const CrawlerStatusIndicator: React.FC = () => { switch (mostRecentCrawlRequestStatus) { case CrawlerStatus.Success: return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', + + /> ); case CrawlerStatus.Failed: case CrawlerStatus.Canceled: return ( - - {i18n.translate( + + /> ); case CrawlerStatus.Pending: case CrawlerStatus.Suspended: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx new file mode 100644 index 0000000000000..6d9f1cd7be64b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { ReactWrapper, shallow } from 'enzyme'; + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiResizeObserver, +} from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { StartCrawlContextMenu } from './start_crawl_context_menu'; + +const MOCK_ACTIONS = { + startCrawl: jest.fn(), + showModal: jest.fn(), +}; + +describe('StartCrawlContextMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('is initially closed', () => { + const wrapper = shallow(); + + expect(wrapper.is(EuiPopover)).toBe(true); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + describe('user actions', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + wrapper = mountWithIntl(); + + wrapper.find(EuiButton).simulate('click'); + + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); + + it('can be opened', () => { + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(menuItems.length).toEqual(2); + }); + + it('can start crawls', () => { + menuItems.at(0).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalled(); + }); + + it('can open a modal to start a crawl with selected domains', () => { + menuItems.at(1).simulate('click'); + + expect(MOCK_ACTIONS.showModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx new file mode 100644 index 0000000000000..1182a845bd4f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from '../crawl_select_domains_modal/crawl_select_domains_modal_logic'; + +interface Props { + menuButtonLabel?: string; + fill?: boolean; +} + +export const StartCrawlContextMenu: React.FC = ({ menuButtonLabel, fill }) => { + const { startCrawl } = useActions(CrawlerLogic); + const { showModal: showCrawlSelectDomainsModal } = useActions(CrawlSelectDomainsModalLogic); + + const [isPopoverOpen, setPopover] = useState(false); + + const togglePopover = () => setPopover(!isPopoverOpen); + + const closePopover = () => setPopover(false); + + return ( + + {menuButtonLabel} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + closePopover(); + startCrawl(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel', + { + defaultMessage: 'Crawl all domains on this engine', + } + )} + , + { + closePopover(); + showCrawlSelectDomainsModal(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlSelectDomainsMenuLabel', + { + defaultMessage: 'Crawl select domains', + } + )} + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index e622798e688ab..59ec64c69d5a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -226,7 +226,7 @@ describe('CrawlerLogic', () => { CrawlerStatus.Running, CrawlerStatus.Canceling, ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { + it(`creates a new timeout for most recent crawl request status ${status}`, async () => { jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce( Promise.resolve({ @@ -260,6 +260,27 @@ describe('CrawlerLogic', () => { expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); + + it('clears the timeout if no events are active', async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + events: [ + { + status: CrawlerStatus.Failed, + crawl_config: {}, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + }); }); it('calls flashApiErrors when there is an error on the request for crawler data', async () => { @@ -276,23 +297,36 @@ describe('CrawlerLogic', () => { describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawler data', async () => { + it('creates a new crawl request, fetches latest crawler data, then marks the request complete', async () => { jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/crawl_requests' + '/internal/app_search/engines/some-engine/crawler/crawl_requests', + { body: JSON.stringify({ overrides: {} }) } ); expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); }); }); itShowsServerErrorAsFlashMessage(http.post, () => { CrawlerLogic.actions.startCrawl(); }); + + it('marks the request complete even after an error', async () => { + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); + http.post.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); + }); }); describe('stopCrawl', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 08a01af67ece6..d68dbc59f06d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -48,7 +48,8 @@ interface CrawlerActions { fetchCrawlerData(): void; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - startCrawl(): void; + onStartCrawlRequestComplete(): void; + startCrawl(overrides?: object): { overrides?: object }; stopCrawl(): void; } @@ -60,7 +61,8 @@ export const CrawlerLogic = kea>({ fetchCrawlerData: true, onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - startCrawl: () => null, + onStartCrawlRequestComplete: true, + startCrawl: (overrides) => ({ overrides }), stopCrawl: () => null, }, reducers: { @@ -135,15 +137,19 @@ export const CrawlerLogic = kea>({ actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, - startCrawl: async () => { + startCrawl: async ({ overrides = {} }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { - await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); + await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`, { + body: JSON.stringify({ overrides }), + }); actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); + } finally { + actions.onStartCrawlRequestComplete(); } }, stopCrawl: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 4d72b854bddfb..509346542ae13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -23,6 +23,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -215,4 +216,10 @@ describe('CrawlerOverview', () => { expect(wrapper.find(AddDomainFormErrors)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index c68e75790f073..f1f25dfb4dc55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -24,6 +24,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -138,6 +139,7 @@ export const CrawlerOverview: React.FC = () => { )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index ed445b923ea2a..addf4093a167b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { getPageHeaderActions } from '../../../test_helpers'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -92,4 +93,10 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index a4b2a9709cd62..63b9c3f080ec2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -17,6 +17,7 @@ import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -78,6 +79,7 @@ export const CrawlerSingleDomain: React.FC = () => { + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index c9212bca322d7..fe225f62d1dce 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -141,6 +141,19 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); + it('validates correctly with overrides', () => { + const request = { + params: { name: 'some-engine' }, + body: { overrides: { domain_allowlist: [] } }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with empty overrides', () => { + const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; + mockRouter.shouldValidate(request); + }); + it('fails validation without name', () => { const request = { params: {} }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f0fdc5c16098b..5adffe1ff3ee5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -63,6 +63,13 @@ export function registerCrawlerRoutes({ params: schema.object({ name: schema.string(), }), + body: schema.object({ + overrides: schema.maybe( + schema.object({ + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }), }, }, enterpriseSearchRequestHandler.createRequest({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 743ff40ecf5e6..98e96ce598561 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -206,7 +206,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { updatedAgentPolicy: NewAgentPolicy ) => { if (selectedTab === SelectedPolicyTab.NEW) { - if (!updatedAgentPolicy.name || !updatedAgentPolicy.namespace) { + if ( + !updatedAgentPolicy.name || + updatedAgentPolicy.name.trim() === '' || + !updatedAgentPolicy.namespace || + updatedAgentPolicy.namespace.trim() === '' + ) { setHasAgentPolicyError(true); } else { setHasAgentPolicyError(false); diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index bf386e7f463a7..472f7378028bc 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -133,7 +133,7 @@ export const checkVersionIsSame = (version: string, kibanaVersion: string) => { const checkSourceUriAllowed = (sourceUri?: string) => { if (sourceUri && !appContextService.getConfig()?.developer?.allowAgentUpgradeSourceUri) { throw new Error( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); } }; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts index 5b9a1bf1539f0..51a516e68ad6d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts @@ -8,7 +8,7 @@ jest.mock('timers/promises'); import { setTimeout } from 'timers/promises'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { retryTransientEsErrors } from './retry'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts index feab9cb882393..ee6d7086cdd3c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import type { Logger } from 'src/core/server'; import { appContextService } from '../../../app_context'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index eba645ae1aae4..554105bb00a92 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -6,7 +6,7 @@ */ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../../../services'; 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 f52d594d07ded..927b7cb75816c 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 @@ -9,7 +9,7 @@ import { readFileSync } from 'fs'; import path from 'path'; import { safeLoad } from 'js-yaml'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { createAppContextStartContractMock } from '../../../../mocks'; 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 94e2e3f6d73af..144bd2240aa01 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 @@ -21,7 +21,7 @@ jest.mock('./common', () => { import { errors } from '@elastic/elasticsearch'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { ElasticsearchAssetType } from '../../../../types'; import type { Installation, RegistryPackage } from '../../../../types'; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index fb92b341928da..97ee5acc71023 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -7,7 +7,7 @@ jest.mock('../../routes/security'); -import type { MockedLogger } from '@kbn/logging/target_types/mocks'; +import type { MockedLogger } from '@kbn/logging-mocks'; import type { ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index f1ac8382a9ba7..5bbfb4f9d15ab 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -7,7 +7,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { DEFAULT_SPACE_ID } from '../../../../../spaces/common/constants'; diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index e1398aea63634..d15d73fca7332 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -11,9 +11,15 @@ import { agentPolicyStatuses, dataTypes } from '../../../common'; import { PackagePolicySchema, NamespaceSchema } from './package_policy'; +function validateNonEmptyString(val: string) { + if (val.trim() === '') { + return 'Invalid empty string'; + } +} + export const AgentPolicyBaseSchema = { id: schema.maybe(schema.string()), - name: schema.string({ minLength: 1 }), + name: schema.string({ minLength: 1, validate: validateNonEmptyString }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), is_managed: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 7e90618079021..25930c07fcd8b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -101,7 +101,7 @@ exports[`extend index management ilm summary extension should return extension w Object { "aliases": "none", "documents": 2, - "documents_deleted": "0", + "documents_deleted": 0, "health": "yellow", "hidden": false, "ilm": Object { @@ -649,7 +649,7 @@ exports[`extend index management ilm summary extension should return extension w Object { "aliases": "none", "documents": 2, - "documents_deleted": "0", + "documents_deleted": 0, "health": "yellow", "hidden": false, "ilm": Object { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 240cc18fdc02d..eaebd6381d984 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -44,7 +44,7 @@ const indexWithoutLifecyclePolicy: Index = { primary: 1, replica: 1, documents: 1, - documents_deleted: '0', + documents_deleted: 0, size: '3.4kb', primary_size: '3.4kb', aliases: 'none', @@ -64,7 +64,7 @@ const indexWithLifecyclePolicy: Index = { primary: 1, replica: 1, documents: 2, - documents_deleted: '0', + documents_deleted: 0, size: '6.5kb', primary_size: '6.5kb', aliases: 'none', @@ -92,7 +92,7 @@ const indexWithLifecycleError = { primary: 1, replica: 1, documents: 2, - documents_deleted: '0', + documents_deleted: 0, size: '6.5kb', primary_size: '6.5kb', aliases: 'none', diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index 28877dee77e57..ba7b58453c36b 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -59,7 +59,9 @@ export interface Index { primary?: number | string; replica?: number | string; documents: number; + documents_deleted: number; size: string; + primary_size: string; isFrozen: boolean; hidden: boolean; aliases: string | string[]; diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts index 759819d8cb1fa..900e3ecc32501 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts @@ -178,6 +178,7 @@ describe('[Index management API Routes] fetch indices lib function', () => { status: undefined, documents: 0, size: '0b', + primary_size: '0b', }), ], }); 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 7b25956703db2..f4b39784dde22 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -62,7 +62,9 @@ async function fetchIndicesCall( primary: indexData.settings?.index?.number_of_shards, replica: indexData.settings?.index?.number_of_replicas, documents: indexStats?.total?.docs?.count ?? 0, + documents_deleted: indexStats?.total?.docs?.deleted ?? 0, size: new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(), + primary_size: new ByteSizeValue(indexStats?.primaries?.store?.size_in_bytes ?? 0).toString(), // @ts-expect-error isFrozen: indexData.settings?.index?.frozen === 'true', aliases: aliases.length ? aliases : 'none', diff --git a/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts index 51793bf4d6347..bffbf858d96a0 100644 --- a/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts +++ b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts @@ -23,7 +23,8 @@ export const createTestIndexStats = (index?: Partial) health: 'green', status: 'open', uuid: 'test_index', - total: { docs: { count: 1 }, store: { size_in_bytes: 100 } }, + total: { docs: { count: 1, deleted: 0 }, store: { size_in_bytes: 100 } }, + primaries: { store: { size_in_bytes: 100 } }, ...index, }; }; @@ -33,6 +34,7 @@ export const createTestIndexResponse = (index?: Partial) => { aliases: 'none', data_stream: undefined, documents: 1, + documents_deleted: 0, health: 'green', hidden: false, isFrozen: false, @@ -40,6 +42,7 @@ export const createTestIndexResponse = (index?: Partial) => { primary: 1, replica: 1, size: '100b', + primary_size: '100b', status: 'open', uuid: 'test_index', ...index, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 9301f17f4d99c..df79091612254 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -16,10 +16,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 90f9c508e1038..8f0809f581ad0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -422,7 +422,7 @@ describe('Log threshold executor', () => { processUngroupedResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); // First call, second argument @@ -486,7 +486,7 @@ describe('Log threshold executor', () => { processGroupByResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); expect(alertUpdaterMock.mock.calls.length).toBe(2); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 73d7d1bf95363..5eedaac5f020a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -16,7 +16,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { ActionGroup, ActionGroupIdsOf, - AlertInstance as Alert, + Alert, AlertInstanceContext as AlertContext, AlertInstanceState as AlertState, AlertTypeState as RuleTypeState, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts index f762d694a59e7..0fb2ff87fd02c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -83,7 +83,7 @@ export const createMetricAnomalyExecutor = typical, influencers, } = first(data as MappedAnomalyHit[])!; - const alert = services.alertInstanceFactory(`${nodeType}-${metric}`); + const alert = services.alertFactory.create(`${nodeType}-${metric}`); alert.scheduleActions(FIRED_ACTIONS_ID, { alertState: stateToAlertMessage[AlertStates.ALERT], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b3c4de9658eda..57001d8cbdb1a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -840,9 +840,9 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId }); const alertInstances = new Map(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { +services.alertFactory.create.mockImplementation((instanceID: string) => { const newAlertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), + instance: alertsMock.createAlertFactory.create(), actionQueue: [], state: {}, }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index f16b8a8135a37..9fbbe26fba126 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -15,10 +15,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, Comparator } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { InfraBackendLibs } from '../../infra_types'; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index ce909152e71b9..feec2117632c0 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -105,15 +105,26 @@ export const pie: ExpressionFunctionDefinition< types: ['number'], help: '', }, + ariaLabel: { + types: ['string'], + help: '', + required: false, + }, }, inputTypes: ['lens_multitable'], - fn(data: LensMultiTable, args: PieExpressionArgs) { + fn(data: LensMultiTable, args: PieExpressionArgs, handlers) { return { type: 'render', as: 'lens_pie_renderer', value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts index 0a7d705ec3fbc..aa84488dbc2c2 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts @@ -42,6 +42,7 @@ export type PieExpressionArgs = SharedPieLayerState & { shape: PieChartTypes; hideLabels: boolean; palette: PaletteOutput; + ariaLabel?: string; }; export interface PieExpressionProps { diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts index f00608135820a..4e712f7ca3bf4 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts @@ -37,4 +37,5 @@ export interface XYArgs { fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; + ariaLabel?: string; } diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts index 0e58105447689..00baf894de046 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts @@ -148,14 +148,27 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Show values in legend', }), }, + ariaLabel: { + types: ['string'], + help: i18n.translate('xpack.lens.xyChart.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the xy chart', + }), + required: false, + }, }, - fn(data: LensMultiTable, args: XYArgs) { + fn(data: LensMultiTable, args: XYArgs, handlers) { return { type: 'render', as: 'lens_xy_chart_renderer', value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index a430a72276ca3..ca37580ad682f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -151,6 +151,7 @@ export class Embeddable private errors: ErrorMessage[] | undefined; private inputReloadSubscriptions: Subscription[]; private isDestroyed?: boolean; + private embeddableTitle?: string; private lensInspector: LensInspector; private logError(type: 'runtime' | 'validation') { @@ -188,6 +189,7 @@ export class Embeddable ); const input$ = this.getInput$(); + this.embeddableTitle = this.getTitle(); this.inputReloadSubscriptions = []; @@ -366,7 +368,8 @@ export class Embeddable !isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || !isEqual(containerState.query, this.externalSearchContext.query) || !isEqual(cleanedFilters, this.externalSearchContext.filters) || - this.externalSearchContext.searchSessionId !== containerState.searchSessionId + this.externalSearchContext.searchSessionId !== containerState.searchSessionId || + this.embeddableTitle !== this.getTitle() ) { this.externalSearchContext = { timeRange: containerState.timeRange, @@ -374,6 +377,7 @@ export class Embeddable filters: cleanedFilters, searchSessionId: containerState.searchSessionId, }; + this.embeddableTitle = this.getTitle(); isDirty = true; } return isDirty; @@ -428,7 +432,11 @@ export class Embeddable errors={this.errors} lensInspector={this.lensInspector} searchContext={this.getMergedSearchContext()} - variables={input.palette ? { theme: { palette: input.palette } } : {}} + variables={ + input.palette + ? { theme: { palette: input.palette }, embeddableTitle: this.getTitle() } + : { embeddableTitle: this.getTitle() } + } searchSessionId={this.externalSearchContext.searchSessionId} handleEvent={this.handleEvent} onData$={this.updateActiveData} diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts index 34907c2e93c63..4980adf52e995 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts @@ -293,14 +293,12 @@ describe('heatmap suggestions', () => { title: 'Heat map', hide: true, previewIcon: 'empty', - score: 0.3, + score: 0, }, ]); }); - }); - describe('shows suggestions', () => { - test('when at least one axis and value accessor are available', () => { + test('when at least one axis has a date histogram', () => { expect( getSuggestions({ table: { @@ -357,21 +355,95 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta hide: true, previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + }); + + describe('shows suggestions', () => { + test('when at least one axis and value accessor are available', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', score: 0.6, }, ]); }); - test('when complete configuration has been resolved', () => { + test('when there is a date histogram and a second bucket dimension', () => { expect( getSuggestions({ table: { layerId: 'first', isMultiRow: true, columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, { columnId: 'date-column', operation: { @@ -390,6 +462,71 @@ describe('heatmap suggestions', () => { label: 'Metric', }, }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + yAccessor: 'date-column', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + + test('when complete configuration has been resolved', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, { columnId: 'group-column', operation: { @@ -414,7 +551,7 @@ describe('heatmap suggestions', () => { layerId: 'first', layerType: layerTypes.DATA, shape: 'heatmap', - xAccessor: 'date-column', + xAccessor: 'number-column', yAccessor: 'group-column', valueAccessor: 'metric-column', gridConfig: { @@ -432,8 +569,7 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta - hide: true, + hide: false, previewIcon: 'empty', score: 0.9, }, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts index fac07d322e037..52c7a1bfd6d26 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts @@ -58,9 +58,17 @@ export const getSuggestions: Visualization['getSugges * Hide for: * - reduced and reorder tables * - tables with just a single bucket dimension + * - tables with only date histogram */ + const hasOnlyDatehistogramBuckets = + metrics.length === 1 && + groups.length > 0 && + groups.every((group) => group.operation.dataType === 'date'); const hide = - table.changeType === 'reduced' || table.changeType === 'reorder' || isSingleBucketDimension; + table.changeType === 'reduced' || + table.changeType === 'reorder' || + isSingleBucketDimension || + hasOnlyDatehistogramBuckets; const newState: HeatmapVisualizationState = { shape: CHART_SHAPES.HEATMAP, @@ -74,8 +82,8 @@ export const getSuggestions: Visualization['getSugges gridConfig: { type: HEATMAP_GRID_FUNCTION, isCellLabelVisible: false, - isYAxisLabelVisible: true, - isXAxisLabelVisible: true, + isYAxisLabelVisible: state?.gridConfig?.isYAxisLabelVisible ?? true, + isXAxisLabelVisible: state?.gridConfig?.isXAxisLabelVisible ?? true, isYAxisTitleVisible: state?.gridConfig?.isYAxisTitleVisible ?? false, isXAxisTitleVisible: state?.gridConfig?.isXAxisTitleVisible ?? false, }, @@ -93,11 +101,15 @@ export const getSuggestions: Visualization['getSugges newState.xAccessor = histogram[0]?.columnId || ordinal[0]?.columnId; newState.yAccessor = groups.find((g) => g.columnId !== newState.xAccessor)?.columnId; - if (newState.xAccessor) { - score += 0.3; - } - if (newState.yAccessor) { - score += 0.3; + const hasDatehistogram = groups.some((group) => group.operation.dataType === 'date'); + + if (!hasDatehistogram) { + if (newState.xAccessor) { + score += 0.3; + } + if (newState.yAccessor) { + score += 0.3; + } } return [ @@ -106,8 +118,7 @@ export const getSuggestions: Visualization['getSugges title: i18n.translate('xpack.lens.heatmap.heatmapLabel', { defaultMessage: 'Heat map', }), - // Temp hide all suggestions while heatmap is in beta - hide: true || hide, + hide, previewIcon: 'empty', score: Number(score.toFixed(1)), }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 008ab9a9cae9e..15706e69d1e16 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -333,6 +333,8 @@ export function PieComponent( legendAction={props.interactive ? getLegendAction(firstTable, onClickValue) : undefined} theme={[themeOverrides, chartTheme]} baseTheme={chartBaseTheme} + ariaLabel={props.args.ariaLabel} + ariaUseDefaultSummary={!props.args.ariaLabel} /> , - attributes?: Partial> + attributes?: Partial> ): Ast | null => { const datasource = datasourceLayers[state.layerId]; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 1402cd715283a..b34d5e8639382 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -6,6 +6,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` > = ({ stat }) => { return ( - {stat.label}: {stat.value} + {stat.label}:{' '} + {stat.value} ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx index f9d734f7b84d5..457bca80ee2c0 100644 --- a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx @@ -31,6 +31,7 @@ export const GettingStartedCallout: FC = () => { return ( <> { />

- + = ({ compactView = false }) => { label: i18n.translate('xpack.ml.trainedModels.nodesList.totalAmountLabel', { defaultMessage: 'Total machine learning nodes', }), + 'data-test-subj': 'mlTotalNodesCount', }, }; }, [items]); @@ -189,7 +190,7 @@ export const NodesList: FC = ({ compactView = false }) => { } return ( - <> +

{nodesStats && ( @@ -218,6 +219,6 @@ export const NodesList: FC = ({ compactView = false }) => { data-test-subj={isLoading ? 'mlNodesTable loading' : 'mlNodesTable loaded'} />
- + ); }; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index e30ea01b27cb5..68a86a927ac1a 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -139,7 +139,7 @@ export function registerAnomalyDetectionAlertType({ if (executionResult) { const alertInstanceName = executionResult.name; - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult); } }, diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 5fd21d5372d23..1173f92930128 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -159,7 +159,7 @@ export function registerJobsMonitoringRuleType({ ); executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); }); } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 0d3d0e9e39cdc..65fa3769d0fed 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -34,7 +34,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); return typeof body.hits.total === 'number' ? body.hits.total : body.hits.total!.value; } @@ -75,7 +75,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); const catCounts: Array<{ @@ -123,7 +123,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); // @ts-expect-error incorrect search response type diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 9f1aecfdb2978..5ffe8ed5eb481 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -214,7 +214,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust sort: [{ record_score: { order: 'desc' } }], }, }, - [] + jobIds ); const tableData: { @@ -345,7 +345,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }; - const { body } = await mlClient.anomalySearch(query, []); + const { body } = await mlClient.anomalySearch(query, jobIds); const maxScore = get(body, ['aggregations', 'max_score', 'value'], null); return { maxScore }; @@ -409,7 +409,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + jobIds ); const bucketsByJobId: Array<{ key: string; maxTimestamp: { value?: number } }> = get( @@ -440,7 +440,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); const examplesByCategoryId: { [key: string]: any } = {}; @@ -477,7 +477,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); const definition = { categoryId, terms: null, regex: null, examples: [] }; @@ -526,7 +526,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); return body ? body.hits.hits.map((r) => r._source) : []; } @@ -617,7 +617,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust aggs, }, }, - [] + jobIds ); if (fieldToBucket === JOB_ID) { finalResults = { diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index d13e6d9ed7f9b..0c48fed40ee34 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -10,11 +10,16 @@ import { i18n } from '@kbn/i18n'; import { RuleType, AlertExecutorOptions, - AlertInstance, + Alert, RulesClient, AlertServices, } from '../../../alerting/server'; -import { Alert, AlertTypeParams, RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; +import { + Alert as Rule, + AlertTypeParams, + RawAlertInstance, + SanitizedAlert, +} from '../../../alerting/common'; import { ActionsClient } from '../../../actions/server'; import { AlertState, @@ -121,7 +126,7 @@ export class BaseRule { }); if (existingRuleData.total > 0) { - return existingRuleData.data[0] as Alert; + return existingRuleData.data[0] as Rule; } const ruleActions = []; @@ -272,7 +277,7 @@ export class BaseRule { for (const node of nodes) { const newAlertStates: AlertNodeState[] = []; // quick fix for now so that non node level alerts will use the cluster id - const instance = services.alertInstanceFactory( + const instance = services.alertFactory.create( node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid ); @@ -331,7 +336,7 @@ export class BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, instanceState: AlertInstanceState | AlertState | unknown, item: AlertData | unknown, cluster?: AlertCluster | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index ed4ba69b8e254..d4f9284b40f8f 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -116,13 +116,15 @@ describe('CCRReadExceptionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 705d0c6b9c87f..e072602d6b711 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, CCRReadExceptionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CCR_READ_EXCEPTIONS, RULE_DETAILS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -209,7 +209,7 @@ export class CCRReadExceptionsRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 85030657825c4..ec4c15afe6731 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -81,13 +81,15 @@ describe('ClusterHealthRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index b8810196c833a..a40fafc65d636 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -18,7 +18,7 @@ import { AlertClusterHealth, AlertInstanceState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CLUSTER_HEALTH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -111,7 +111,7 @@ export class ClusterHealthRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index bcd2c0cbb5810..cf2e0f29ddbc3 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('CpuUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index fa4b64fd997c3..08a5cdb6c2780 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -22,7 +22,7 @@ import { CommonAlertParams, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -145,7 +145,7 @@ export class CpuUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index daaded1c18c80..c08d32c395c1b 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -96,13 +96,15 @@ describe('DiskUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index 1e06f0649d107..a52a2fd79d654 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertDiskUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -152,7 +152,7 @@ export class DiskUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 4531c5f0f1ffc..560ab805a236c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -85,13 +85,15 @@ describe('ElasticsearchVersionMismatchAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 9d89f827f9b10..43f5be14538b6 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index b4444c9088073..b136d2c71f065 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -88,13 +88,15 @@ describe('KibanaVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 24182c4a545d3..4e7a688b92ca9 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_KIBANA_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -97,7 +97,7 @@ export class KibanaVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index 0460064b4f7c5..a82b87cfe8a97 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -96,13 +96,15 @@ describe('LargeShardSizeRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 92be43b9c06c0..fbcf557a1f6f5 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, IndexShardSizeStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LARGE_SHARD_SIZE, RULE_DETAILS } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -149,7 +149,7 @@ export class LargeShardSizeRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 86a6f666fcf87..0a69ee68ffeba 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -86,13 +86,15 @@ describe('LicenseExpirationRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 3a837a125a523..ad13ca9c56dfa 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -20,7 +20,7 @@ import { AlertLicense, AlertLicenseState, } from '../../common/types/alerts'; -import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; +import { AlertExecutorOptions, Alert } from '../../../alerting/server'; import { RULE_LICENSE_EXPIRATION, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -143,7 +143,7 @@ export class LicenseExpirationRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 857a9bf5bfa79..b7790f81caa3e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -86,13 +86,15 @@ describe('LogstashVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index ee3e5838d7d35..bca82de1a5fae 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LOGSTASH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class LogstashVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 6e7aff2ae8fb4..785b1013304cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('MemoryUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 06ecf4bb450c8..62f790b1eb6d0 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertMemoryUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MEMORY_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -158,7 +158,7 @@ export class MemoryUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index a8a96a61a4b25..b70bfe4bfb375 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -87,13 +87,15 @@ describe('MissingMonitoringDataRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index fa7cbe009712a..9002855e2b67f 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -19,7 +19,7 @@ import { CommonAlertFilter, AlertNodeState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -137,7 +137,7 @@ export class MissingMonitoringDataRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index 3e24df3a6ef15..3704e909101e9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -137,13 +137,15 @@ describe('NodesChangedAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 82cf91e91b52a..3b14cf2428889 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -19,7 +19,7 @@ import { AlertInstanceState, AlertNodesChangedState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_NODES_CHANGED, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; @@ -174,7 +174,7 @@ export class NodesChangedRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index 0cca5eb81c95f..ca1b78a62646a 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -20,10 +20,10 @@ import { AlertState, AlertThreadPoolRejectionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -import { Alert, RawAlertInstance } from '../../../alerting/common'; +import { Alert as Rule, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; import { Globals } from '../static_globals'; @@ -47,7 +47,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { } constructor( - sanitizedRule: Alert | undefined = undefined, + sanitizedRule: Rule | undefined = undefined, public readonly id: string, public readonly threadPoolType: string, public readonly name: string, @@ -176,7 +176,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { }; } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 63a02088b9b65..45f8caacbcd41 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolSearchRejectionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index da4c7ffaeffa0..47f6704eae70f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolWriteRejectionsAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 458bcc0ea4a5f..b1a1b55b7ed1e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -50,8 +50,6 @@ describe('ExploratoryView', () => { expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument(); expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); - expect(await screen.findByRole('heading', { name: /Explore data/i })).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { @@ -62,4 +60,17 @@ describe('ExploratoryView', () => { expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument(); }); + + it('shows/hides the chart', async () => { + render(); + expect(screen.queryByText('Refresh')).toBeInTheDocument(); + + const toggleButton = await screen.findByText('Hide chart'); + expect(toggleButton).toBeInTheDocument(); + + toggleButton.click(); + + expect(toggleButton.textContent).toBe('Show chart'); + expect(screen.queryByText('Refresh')).toBeNull(); + }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 62b54a3fec203..a383bc37880ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -9,17 +9,16 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { + EuiButton, EuiButtonEmpty, EuiResizableContainer, EuiTitle, - EuiPanel, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; -import { ExploratoryViewHeader } from './header/header'; import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; @@ -27,7 +26,9 @@ import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -import type { ChartTimeRange } from './header/last_updated'; +import { ChartTimeRange, LastUpdated } from './header/last_updated'; +import { useExpViewTimeRange } from './hooks/use_time_range'; +import { ExpViewActionMenu } from './components/action_menu'; export type PanelId = 'seriesPanel' | 'chartPanel'; @@ -53,9 +54,10 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, allSeries, lastRefresh, reportType, setLastRefresh } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); + const timeRange = useExpViewTimeRange(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -100,80 +102,85 @@ export function ExploratoryView({ } }; - return ( - - {lens ? ( - <> - - - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - + return lens ? ( + <> + + + + {(EuiResizablePanel, _EuiResizableButton, { togglePanel }) => { + collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); + + return ( + <> + + + onChange('chartPanel')} + > + {hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL} + + + {hiddenPanel === 'chartPanel' ? null : ( + <> + + + - onChange('chartPanel')} - > - {hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL} - + setLastRefresh(Date.now())}> + {REFRESH_LABEL} + - - - - {lensAttributes ? ( - - ) : ( - - )} - - - - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - - )} - - - ) : ( - -

{LENS_NOT_AVAILABLE}

-
- )} -
+ + )} + + + + {lensAttributes ? ( + + ) : ( + + )} + + + + + + + ); + }} + + {hiddenPanel === 'seriesPanel' && ( + onChange('seriesPanel')} iconType="arrowUp"> + {PREVIEW_LABEL} + + )} + + + ) : ( + +

{LENS_NOT_AVAILABLE}

+
); } const LensWrapper = styled.div<{ height: string }>` @@ -192,19 +199,6 @@ const ResizableContainer = styled(EuiResizableContainer)` } `; -const Wrapper = styled(EuiPanel)` - max-width: 1800px; - min-width: 800px; - margin: 0 auto; - width: 100%; - overflow-x: auto; - position: relative; - - .echLegendItem__action { - display: none; - } -`; - const ShowPreview = styled(EuiButtonEmpty)` position: absolute; bottom: 34px; @@ -222,6 +216,10 @@ const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryVi defaultMessage: 'Preview', }); +const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { + defaultMessage: 'Refresh', +}); + const LENS_NOT_AVAILABLE = i18n.translate( 'xpack.observability.overview.exploratoryView.lensDisabled', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx deleted file mode 100644 index 82055ba128c9d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '../rtl_helpers'; -import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('ExploratoryViewHeader', function () { - it('should render properly', function () { - const { getByText } = render( - - ); - getByText('Refresh'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx deleted file mode 100644 index 8ef3f228a5854..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { ExpViewActionMenu } from '../components/action_menu'; -import { useExpViewTimeRange } from '../hooks/use_time_range'; -import { LastUpdated } from './last_updated'; -import type { ChartTimeRange } from './last_updated'; - -interface Props { - chartTimeRange?: ChartTimeRange; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} - -export function ExploratoryViewHeader({ lensAttributes, chartTimeRange }: Props) { - const { setLastRefresh } = useSeriesStorage(); - - const timeRange = useExpViewTimeRange(); - - return ( - <> - - - - -

- {i18n.translate('xpack.observability.expView.heading.label', { - defaultMessage: 'Explore data', - })} -

-
-
- - - - - setLastRefresh(Date.now())}> - {REFRESH_LABEL} - - -
- - ); -} - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 5dbe0c2a6c078..4fc5293e03723 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { useHistory } from 'react-router-dom'; import { ExploratoryView } from './exploratory_view'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; @@ -22,6 +21,11 @@ import { import { UrlStorageContextProvider } from './hooks/use_series_storage'; import { useTrackPageview } from '../../..'; import { TypedLensByValueInput } from '../../../../../lens/public'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +const PAGE_TITLE = i18n.translate('xpack.observability.expView.heading.label', { + defaultMessage: 'Explore data', +}); export interface ExploratoryViewPageProps { useSessionStorage?: boolean; @@ -52,6 +56,7 @@ export function ExploratoryViewPage({ app ); + const { ObservabilityPageTemplate } = usePluginContext(); const { services: { uiSettings, notifications }, } = useKibana(); @@ -67,19 +72,15 @@ export function ExploratoryViewPage({ }); return ( - + - + ); } -const Wrapper = euiStyled.div` - padding: ${(props) => props.theme.eui.paddingSizes.l}; -`; - // eslint-disable-next-line import/no-default-export export default ExploratoryViewPage; diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 9543039ab576a..ad7a947273a23 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Collector, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts index d67c755cb6bc1..0685fcc5bab93 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts @@ -6,7 +6,7 @@ */ import { ResourceInstaller } from './resource_installer'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { Dataset } from './index_options'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts index 897af855bd4b2..befc42278196d 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { RuleDataService } from './rule_data_plugin_service'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index d1c20e0667e24..1768f0666fd51 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { ALERT_INSTANCE_ID, ALERT_RULE_CATEGORY, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 9ae3dff28b2ae..aea27787af080 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -13,7 +13,7 @@ import { v4 } from 'uuid'; import { difference } from 'lodash'; import { AlertExecutorOptions, - AlertInstance, + Alert, AlertInstanceContext, AlertInstanceState, AlertTypeParams, @@ -62,7 +62,7 @@ export type LifecycleAlertService< > = (alert: { id: string; fields: ExplicitAlertFields; -}) => AlertInstance; +}) => Alert; export interface LifecycleAlertServices< InstanceState extends AlertInstanceState = never, @@ -143,7 +143,7 @@ export const createLifecycleExecutor = > ): Promise> => { const { - services: { alertInstanceFactory, shouldWriteAlerts }, + services: { alertFactory, shouldWriteAlerts }, state: previousState, } = options; @@ -165,7 +165,7 @@ export const createLifecycleExecutor = > = { alertWithLifecycle: ({ id, fields }) => { currentAlerts[id] = fields; - return alertInstanceFactory(id); + return alertFactory.create(id); }, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 05a71677c7535..baa60664dea57 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -13,7 +13,7 @@ import { ALERT_STATUS_RECOVERED, ALERT_UUID, } from '@kbn/rule-data-utils'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { castArray, omit } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; @@ -66,10 +66,12 @@ function createRule(shouldWriteAlerts: boolean = true) { const scheduleActions = jest.fn(); - const alertInstanceFactory = () => { - return { - scheduleActions, - } as any; + const alertFactory = { + create: () => { + return { + scheduleActions, + } as any; + }, }; return { @@ -107,7 +109,7 @@ function createRule(shouldWriteAlerts: boolean = true) { updatedBy: 'updatedBy', }, services: { - alertInstanceFactory, + alertFactory, savedObjectsClient: {} as any, scopedClusterClient: {} as any, shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts index 37b4847bc9c69..5513aaf532522 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -34,5 +34,5 @@ export const createLifecycleAlertServicesMock = < >( alertServices: AlertServices ): LifecycleAlertServices => ({ - alertWithLifecycle: ({ id }) => alertServices.alertInstanceFactory(id), + alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), }); diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 08b1b0a8ecbf2..3d880988182b1 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -67,8 +67,7 @@ export const createDefaultAlertExecutorOptions = < params, spaceId: 'SPACE_ID', services: { - alertInstanceFactory: alertsMock.createAlertServices() - .alertInstanceFactory, + alertFactory: alertsMock.createAlertServices().alertFactory, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 53c4e5b882406..7c10fd63b463a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -10,19 +10,16 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { enableRules } from '../../../containers/detection_engine/rules'; -import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; import { RuleSwitchComponent } from './index'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; -import { useRulesTableContextOptional } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useRulesTableContextMock } from '../../../containers/detection_engine/rules/rules_table/__mocks__/rules_table_context'; +import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; +import { useRulesTableContextMock } from '../../../pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context'; import { TestProviders } from '../../../../common/mock'; jest.mock('../../../../common/components/toasters'); jest.mock('../../../containers/detection_engine/rules'); -jest.mock('../../../containers/detection_engine/rules/rules_table/rules_table_context'); -jest.mock('../../../pages/detection_engine/rules/all/actions'); +jest.mock('../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); describe('RuleSwitch', () => { beforeEach(() => { @@ -36,77 +33,43 @@ describe('RuleSwitch', () => { }); test('it renders loader if "isLoading" is true', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="ruleSwitch"]').exists()).toBeFalsy(); }); test('it renders switch disabled if "isDisabled" is true', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); }); test('it renders switch enabled if "enabled" is true', () => { - const wrapper = mount(, { + const wrapper = mount(, { wrappingComponent: TestProviders, }); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); }); test('it renders switch disabled if "enabled" is false', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); - }); - - test('it renders an off switch enabled on click', async () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeTruthy(); - }); - }); - - test('it renders an on switch off on click', async () => { - const rule: RulesSchema = { ...getRulesSchemaMock(), enabled: false }; - - (enableRules as jest.Mock).mockResolvedValue([rule]); - - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeFalsy(); + const wrapper = mount(, { + wrappingComponent: TestProviders, }); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); }); test('it dispatches error toaster if "enableRules" call rejects', async () => { const mockError = new Error('uh oh'); (enableRules as jest.Mock).mockRejectedValue(mockError); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { @@ -122,10 +85,9 @@ describe('RuleSwitch', () => { { error: { status_code: 400, message: 'error' } }, ]); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { @@ -134,18 +96,18 @@ describe('RuleSwitch', () => { }); }); - test('it invokes "enableRulesAction" if in rules table context', async () => { - (useRulesTableContextOptional as jest.Mock).mockReturnValue(useRulesTableContextMock.create()); + test('it calls "setLoadingRules" if in rules table context', async () => { + const rulesTableContext = useRulesTableContextMock.create(); + (useRulesTableContextOptional as jest.Mock).mockReturnValue(rulesTableContext); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { wrapper.update(); - expect(enableRulesAction).toHaveBeenCalledTimes(1); + expect(rulesTableContext.actions.setLoadingRules).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index e596c552e623f..893a0d4d8de8b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -12,17 +12,12 @@ import { EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import React, { useMemo, useCallback, useState, useEffect } from 'react'; - -import * as i18n from '../../../pages/detection_engine/rules/translations'; -import { enableRules } from '../../../containers/detection_engine/rules'; +import { useStateToaster } from '../../../../common/components/toasters'; +import { useUpdateRulesCache } from '../../../containers/detection_engine/rules/use_find_rules_query'; import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; -import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; -import { bucketRulesResponse } from '../../../pages/detection_engine/rules/all/helpers'; -import { useRulesTableContextOptional } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useInvalidateRules } from '../../../containers/detection_engine/rules/rules_table/use_find_rules'; +import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -38,7 +33,6 @@ export interface RuleSwitchProps { enabled: boolean; isDisabled?: boolean; isLoading?: boolean; - optionLabel?: string; onChange?: (enabled: boolean) => void; } @@ -50,70 +44,31 @@ export const RuleSwitchComponent = ({ isDisabled, isLoading, enabled, - optionLabel, onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); - const [myEnabled, setMyEnabled] = useState(enabled ?? false); const [, dispatchToaster] = useStateToaster(); const rulesTableContext = useRulesTableContextOptional(); - const invalidateRules = useInvalidateRules(); + const updateRulesCache = useUpdateRulesCache(); const onRuleStateChange = useCallback( async (event: EuiSwitchEvent) => { setMyIsLoading(true); - if (rulesTableContext != null) { - await enableRulesAction( - [id], - event.target.checked, - dispatchToaster, - rulesTableContext.actions.setLoadingRules - ); - } else { - const enabling = event.target.checked; - const title = enabling - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); - try { - const response = await enableRules({ - ids: [id], - enabled: enabling, - }); - const { rules, errors } = bucketRulesResponse(response); - - if (errors.length > 0) { - setMyIsLoading(false); - - displayErrorToast( - title, - errors.map((e) => e.error.message), - dispatchToaster - ); - } else { - const [rule] = rules; - setMyEnabled(rule.enabled); - if (onChange != null) { - onChange(rule.enabled); - } - } - } catch (err) { - setMyIsLoading(false); - displayErrorToast(title, err.message, dispatchToaster); - } + const rules = await enableRulesAction( + [id], + event.target.checked, + dispatchToaster, + rulesTableContext?.actions.setLoadingRules + ); + if (rules?.[0]) { + updateRulesCache(rules); + onChange?.(rules[0].enabled); } - invalidateRules(); setMyIsLoading(false); }, - [dispatchToaster, id, invalidateRules, onChange, rulesTableContext] + [dispatchToaster, id, onChange, rulesTableContext?.actions.setLoadingRules, updateRulesCache] ); - useEffect(() => { - if (myEnabled !== enabled) { - setMyEnabled(enabled); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enabled]); - const showLoader = useMemo((): boolean => { if (myIsLoading !== isLoading) { return isLoading || myIsLoading; @@ -130,10 +85,9 @@ export const RuleSwitchComponent = ({ ) : ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts deleted file mode 100644 index 5088ea8a3e2b0..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback } from 'react'; -import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from 'react-query'; -import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; -import { fetchRules } from '../api'; -import * as i18n from '../translations'; -import { FilterOptions, PaginationOptions, Rule, SortingOptions } from '../types'; - -interface FindRulesQueryArgs { - filterOptions?: FilterOptions; - sortingOptions?: SortingOptions; - pagination?: Pick; -} - -interface UseFindRulesArgs extends FindRulesQueryArgs { - isInMemorySorting: boolean; - refetchInterval: number | false; -} - -const MAX_RULES_PER_PAGE = 10000; -const FIND_RULES_QUERY_KEY = 'findRules'; - -/** - * This hook is used to fetch detection rules. Under the hood, it implements a - * "feature switch" that allows switching from an in-memory implementation to a - * backend-based implementation on the fly. - * - * @param args - find rules arguments - * @returns rules query result - */ -export const useFindRules = (args: UseFindRulesArgs) => { - const { pagination, filterOptions, sortingOptions, isInMemorySorting, refetchInterval } = args; - - // Use this query result when isInMemorySorting = true - const allRules = useFindRulesQuery( - getFindRulesQueryKey({ pagination, filterOptions, sortingOptions, isInMemorySorting: true }), - { pagination: { page: 1, perPage: MAX_RULES_PER_PAGE } }, - { refetchInterval, enabled: isInMemorySorting } - ); - - // Use this query result when isInMemorySorting = false - const pagedRules = useFindRulesQuery( - getFindRulesQueryKey({ pagination, filterOptions, sortingOptions, isInMemorySorting: false }), - { pagination, filterOptions, sortingOptions }, - { - refetchInterval, - enabled: !isInMemorySorting, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - } - ); - - return isInMemorySorting ? allRules : pagedRules; -}; - -/** - * A helper method used to construct a query key to be used as a cache key for - * react-query - * - * @param args - query arguments - * @returns Query key - */ -export const getFindRulesQueryKey = ({ - isInMemorySorting, - filterOptions, - sortingOptions, - pagination, -}: FindRulesQueryArgs & Pick) => - isInMemorySorting - ? [FIND_RULES_QUERY_KEY, 'all'] // For the in-memory implementation we fetch data only once and cache it, thus the key is constant and do not depend on input arguments - : [FIND_RULES_QUERY_KEY, 'paged', filterOptions, sortingOptions, pagination]; - -interface RulesQueryData { - rules: Rule[]; - total: number; -} - -const useFindRulesQuery = ( - queryKey: QueryKey, - queryArgs: FindRulesQueryArgs, - queryOptions: UseQueryOptions -) => { - const { addError } = useAppToasts(); - - return useQuery( - queryKey, - async ({ signal }) => { - const response = await fetchRules({ signal, ...queryArgs }); - - return { rules: response.data, total: response.total }; - }, - { - refetchIntervalInBackground: false, - onError: (error: Error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), - ...queryOptions, - } - ); -}; - -/** - * We should use this hook to invalidate the rules cache. Any rule mutation, - * like creation, deletion, modification, or rule activation, should lead to - * cache invalidation. - * - * We invalidate all rules cache entries for simplicity so that we don't need to - * look for cache entries that contain mutated rules. - * - * @returns A rules cache invalidation callback - */ -export const useInvalidateRules = () => { - const queryClient = useQueryClient(); - - return useCallback(() => { - /** - * Invalidate all queries that start with FIND_RULES_QUERY_KEY. This - * includes the in-memory query cache and paged query cache. - */ - queryClient.invalidateQueries(FIND_RULES_QUERY_KEY, { - refetchActive: true, - refetchInactive: false, - }); - }, [queryClient]); -}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index 86d10d16a4a23..f71f20cb6c189 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -13,7 +13,7 @@ import { CreateRulesSchema } from '../../../../../common/detection_engine/schema import { createRule } from './api'; import * as i18n from './translations'; import { transformOutput } from './transforms'; -import { useInvalidateRules } from './rules_table/use_find_rules'; +import { useInvalidateRules } from './use_find_rules_query'; interface CreateRuleReturn { isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts new file mode 100644 index 0000000000000..47778be0d9c91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useQuery, useQueryClient, UseQueryOptions } from 'react-query'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { fetchRules } from './api'; +import * as i18n from './translations'; +import { FilterOptions, PaginationOptions, Rule, SortingOptions } from './types'; + +export interface FindRulesQueryArgs { + filterOptions?: FilterOptions; + sortingOptions?: SortingOptions; + pagination?: Pick; +} + +const FIND_RULES_QUERY_KEY = 'findRules'; + +export interface RulesQueryData { + rules: Rule[]; + total: number; +} + +/** + * A wrapper around useQuery provides default values to the underlying query, + * like query key, abortion signal, and error handler. + * + * @param queryPrefix - query prefix used to differentiate the query from other + * findRules queries + * @param queryArgs - fetch rules filters/pagination + * @param queryOptions - react-query options + * @returns useQuery result + */ +export const useFindRulesQuery = ( + queryPrefix: string[], + queryArgs: FindRulesQueryArgs, + queryOptions: UseQueryOptions< + RulesQueryData, + Error, + RulesQueryData, + [...string[], FindRulesQueryArgs] + > +) => { + const { addError } = useAppToasts(); + + return useQuery( + [FIND_RULES_QUERY_KEY, ...queryPrefix, queryArgs], + async ({ signal }) => { + const response = await fetchRules({ signal, ...queryArgs }); + + return { rules: response.data, total: response.total }; + }, + { + refetchIntervalInBackground: false, + onError: (error: Error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), + ...queryOptions, + } + ); +}; + +/** + * We should use this hook to invalidate the rules cache. For example, rule + * mutations that affect rule set size, like creation or deletion, should lead + * to cache invalidation. + * + * @returns A rules cache invalidation callback + */ +export const useInvalidateRules = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + /** + * Invalidate all queries that start with FIND_RULES_QUERY_KEY. This + * includes the in-memory query cache and paged query cache. + */ + queryClient.invalidateQueries(FIND_RULES_QUERY_KEY, { + refetchActive: true, + refetchInactive: false, + }); + }, [queryClient]); +}; + +/** + * We should use this hook to update the rules cache when modifying rules + * without changing the rules collection size. Use it with the new rules data + * after operations like bulk or single rule edit or rule activation, but not + * when adding or removing rules. When adding/removing rules, we should + * invalidate the cache instead. + * + * @returns A rules cache update callback + */ +export const useUpdateRulesCache = () => { + const queryClient = useQueryClient(); + /** + * Use this method to update rules data cached by react-query. + * It is useful when we receive new rules back from a mutation query (bulk edit, etc.); + * we can merge those rules with the existing cache to avoid an extra roundtrip to re-fetch updated rules. + */ + return useCallback( + (newRules: Rule[]) => { + queryClient.setQueriesData['data']>( + FIND_RULES_QUERY_KEY, + (currentData) => + currentData + ? { + rules: updateRules(currentData.rules, newRules), + total: currentData.total, + } + : undefined + ); + }, + [queryClient] + ); +}; + +/** + * Update cached rules with the new ones + * + * @param currentRules + * @param newRules + */ +export function updateRules(currentRules: Rule[], newRules: Rule[]): Rule[] { + const newRulesMap = new Map(newRules.map((rule) => [rule.id, rule])); + + if (currentRules.some((rule) => newRulesMap.has(rule.id))) { + return currentRules.map((rule) => newRulesMap.get(rule.id) ?? rule); + } + + return currentRules; +} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx index aee965a8b8ef0..3f48f55cad4a4 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -14,7 +14,7 @@ import { transformOutput } from './transforms'; import { updateRule } from './api'; import * as i18n from './translations'; -import { useInvalidateRules } from './rules_table/use_find_rules'; +import { useInvalidateRules } from './use_find_rules_query'; interface UpdateRuleReturn { isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts index 951d71162a9a2..5b2fdbc5e290e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts @@ -33,7 +33,7 @@ import { performBulkAction, Rule, } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableActions } from './rules_table/rules_table_context'; import { transformOutput } from '../../../../containers/detection_engine/rules/transforms'; import * as i18n from '../translations'; import { bucketRulesResponse, getExportedRulesCounts } from './helpers'; @@ -135,14 +135,14 @@ export const enableRulesAction = async ( ids: string[], enabled: boolean, dispatchToaster: Dispatch, - setLoadingRules: RulesTableActions['setLoadingRules'] + setLoadingRules?: RulesTableActions['setLoadingRules'] ) => { const errorTitle = enabled ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(ids.length) : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); try { - setLoadingRules({ ids, action: enabled ? 'enable' : 'disable' }); + setLoadingRules?.({ ids, action: enabled ? 'enable' : 'disable' }); const response = await enableRules({ ids, enabled }); const { rules, errors } = bucketRulesResponse(response); @@ -167,10 +167,12 @@ export const enableRulesAction = async ( enabled ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED ); } + + return rules; } catch (e) { displayErrorToast(errorTitle, [e.message], dispatchToaster); } finally { - setLoadingRules({ ids: [], action: null }); + setLoadingRules?.({ ids: [], action: null }); } }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index a31240fae29ae..14b6e394cb146 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -28,7 +28,7 @@ import { import { isMlRule } from '../../../../../../../common/machine_learning/helpers'; import { displayWarningToast, useStateToaster } from '../../../../../../common/components/toasters'; import { canEditRuleWithActions } from '../../../../../../common/utils/privileges'; -import { useRulesTableContext } from '../../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from '../rules_table/rules_table_context'; import * as detectionI18n from '../../../translations'; import * as i18n from '../../translations'; import { @@ -48,7 +48,7 @@ import { convertRulesFilterToKQL } from '../../../../../containers/detection_eng import type { FilterOptions } from '../../../../../containers/detection_engine/rules/types'; import type { BulkActionPartialErrorResponse } from '../../../../../../../common/detection_engine/schemas/response/perform_bulk_action_schema'; import type { HTTPError } from '../../../../../../../common/detection_engine/types'; -import { useInvalidateRules } from '../../../../../containers/detection_engine/rules/rules_table/use_find_rules'; +import { useInvalidateRules } from '../../../../../containers/detection_engine/rules/use_find_rules_query'; interface UseBulkActionsArgs { filterOptions: FilterOptions; @@ -310,7 +310,7 @@ export const useBulkActions = ({ error.stack = JSON.stringify(error.body, null, 2); toasts.addError(error, { title: i18n.BULK_EDIT_ERROR_TOAST_TITLE, - toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCIRPTION(failedRulesCount), + toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCRIPTION(failedRulesCount), }); } catch (e) { // toast error has failed diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts index 1f3dad4d50aae..19f01bddae0f5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts @@ -5,7 +5,7 @@ * 2.0. */ import { useState, useCallback, useRef } from 'react'; -import { useAsyncConfirmation } from '../../../../../containers/detection_engine/rules/rules_table/use_async_confirmation'; +import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; import { BulkActionEditType, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index ba9cb5eded38f..3b24dda539174 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -17,7 +17,7 @@ import { AllRules } from './index'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/rules'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/rules_table_context'); +jest.mock('../../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); const useKibanaMock = useKibana as jest.Mocked; @@ -47,7 +47,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); @@ -67,7 +66,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); @@ -91,7 +89,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 2cf26bbbd23ea..e8c7742125c74 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -20,7 +20,6 @@ interface AllRulesProps { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: () => Promise) => void; } /** @@ -41,7 +40,6 @@ export const AllRules = React.memo( rulesInstalled, rulesNotInstalled, rulesNotUpdated, - setRefreshRulesData, }) => { const [activeTab, setActiveTab] = useState(AllRulesTabs.rules); @@ -59,7 +57,6 @@ export const AllRules = React.memo( rulesNotInstalled={rulesNotInstalled} rulesNotUpdated={rulesNotUpdated} selectedTab={activeTab} - setRefreshRulesData={setRefreshRulesData} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/__mocks__/rules_table_context.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx index 45b7bb1557f36..2bf20acfb9334 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx @@ -6,10 +6,15 @@ */ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants'; -import { invariant } from '../../../../../../common/utils/invariant'; -import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; -import { FilterOptions, PaginationOptions, Rule, SortingOptions } from '../types'; +import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../../common/constants'; +import { invariant } from '../../../../../../../common/utils/invariant'; +import { useKibana, useUiSetting$ } from '../../../../../../common/lib/kibana'; +import { + FilterOptions, + PaginationOptions, + Rule, + SortingOptions, +} from '../../../../../containers/detection_engine/rules/types'; import { useFindRules } from './use_find_rules'; import { getRulesComparator, getRulesPredicate } from './utils'; @@ -102,7 +107,7 @@ export type LoadingRuleAction = | 'edit' | null; -interface LoadingRules { +export interface LoadingRules { ids: string[]; action: LoadingRuleAction; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_async_confirmation.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_async_confirmation.ts diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts new file mode 100644 index 0000000000000..47a2617dd2e25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + useFindRulesQuery, + FindRulesQueryArgs, +} from '../../../../../containers/detection_engine/rules/use_find_rules_query'; + +interface UseFindRulesArgs extends FindRulesQueryArgs { + isInMemorySorting: boolean; + refetchInterval: number | false; +} + +const MAX_RULES_PER_PAGE = 10000; + +/** + * This hook is used to fetch detection rules. Under the hood, it implements a + * "feature switch" that allows switching from an in-memory implementation to a + * backend-based implementation on the fly. + * + * @param args - find rules arguments + * @returns rules query result + */ +export const useFindRules = (args: UseFindRulesArgs) => { + const { pagination, filterOptions, sortingOptions, isInMemorySorting, refetchInterval } = args; + + // Use this query result when isInMemorySorting = true + const allRules = useFindRulesQuery( + ['all'], + { pagination: { page: 1, perPage: MAX_RULES_PER_PAGE } }, + { refetchInterval, enabled: isInMemorySorting } + ); + + // Use this query result when isInMemorySorting = false + const pagedRules = useFindRulesQuery( + ['paged'], + { pagination, filterOptions, sortingOptions }, + { + refetchInterval, + enabled: !isInMemorySorting, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + return isInMemorySorting ? allRules : pagedRules; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts similarity index 96% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts index b3e612537c5d7..37deade0d1316 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts @@ -6,7 +6,11 @@ */ import { get } from 'lodash'; -import { FilterOptions, Rule, SortingOptions } from '../types'; +import { + FilterOptions, + Rule, + SortingOptions, +} from '../../../../../containers/detection_engine/rules/types'; /** * Returns a comparator function to be used with .sort() diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx index a85c5213ca599..3c960108fddf8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx @@ -16,7 +16,7 @@ import { NavigateToAppOptions } from '../../../../../../../../../src/core/public import { ActionToaster } from '../../../../../common/components/toasters'; import { canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { Rule } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableActions } from './rules_table/rules_table_context'; import * as i18n from '../translations'; import { deleteRulesAction, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx index 14a1d93ee66d6..261e14fd1411b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx @@ -8,7 +8,7 @@ import { EuiSwitch, EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; const ToolbarLayout = styled.div` @@ -16,7 +16,7 @@ const ToolbarLayout = styled.div` grid-template-columns: 1fr auto; align-items: center; grid-gap: 16px; - box-shadow: inset 0 -1px 0 #d3dae6; + box-shadow: inset 0 -1px 0 ${({ theme }) => theme.eui.euiBorderColor}; `; interface RulesTableToolbarProps { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index ccf5f5e7407a3..1b8adb0bd11d3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -14,7 +14,7 @@ import { EuiLoadingContent, EuiProgress, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { partition } from 'lodash/fp'; import { AllRulesTabs } from './rules_table_toolbar'; @@ -30,8 +30,8 @@ import { Rule, RulesSortingFields, } from '../../../../containers/detection_engine/rules'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useAsyncConfirmation } from '../../../../containers/detection_engine/rules/rules_table/use_async_confirmation'; +import { useRulesTableContext } from './rules_table/rules_table_context'; +import { useAsyncConfirmation } from './rules_table/use_async_confirmation'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; import { EuiBasicTableOnChange } from '../types'; @@ -59,7 +59,6 @@ interface RulesTableProps { rulesNotInstalled: number | null; rulesNotUpdated: number | null; selectedTab: AllRulesTabs; - setRefreshRulesData: (refreshRule: () => Promise) => void; } const NO_ITEMS_MESSAGE = ( @@ -85,7 +84,6 @@ export const RulesTables = React.memo( rulesNotInstalled, rulesNotUpdated, selectedTab, - setRefreshRulesData, }) => { const { timelines } = useKibana().services; const tableRef = useRef(null); @@ -137,11 +135,11 @@ export const RulesTables = React.memo( onFinish: hideDeleteConfirmation, }); - const [isBulkEditConfirmationVisible, showBulkEditonfirmation, hideBulkEditConfirmation] = + const [isBulkEditConfirmationVisible, showBulkEditConfirmation, hideBulkEditConfirmation] = useBoolState(); const [confirmBulkEdit, handleBulkEditConfirm, handleBulkEditCancel] = useAsyncConfirmation({ - onInit: showBulkEditonfirmation, + onInit: showBulkEditConfirmation, onFinish: hideBulkEditConfirmation, }); @@ -162,8 +160,8 @@ export const RulesTables = React.memo( const hasPagination = pagination.total > pagination.perPage; const [selectedElasticRuleIds, selectedCustomRuleIds] = useMemo(() => { - const ruleImmutablityMap = new Map(rules.map((rule) => [rule.id, rule.immutable])); - const predicate = (id: string) => ruleImmutablityMap.get(id); + const ruleImmutabilityMap = new Map(rules.map((rule) => [rule.id, rule.immutable])); + const predicate = (id: string) => ruleImmutabilityMap.get(id); return partition(predicate, selectedRuleIds); }, [rules, selectedRuleIds]); @@ -210,12 +208,6 @@ export const RulesTables = React.memo( const rulesColumns = useRulesColumns({ hasPermissions }); const monitoringColumns = useMonitoringColumns({ hasPermissions }); - useEffect(() => { - setRefreshRulesData(async () => { - await reFetchRules(); - }); - }, [reFetchRules, setRefreshRulesData]); - const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index f41112e23c4d9..f241a3df87327 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -33,7 +33,7 @@ import { canEditRuleWithActions, getToolTipContent } from '../../../../../common import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { SeverityBadge } from '../../../../components/rules/severity_badge'; import { Rule } from '../../../../containers/detection_engine/rules'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; import { PopoverTooltip } from './popover_tooltip'; import { TableHeaderTooltipCell } from './table_header_tooltip_cell'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx index 7276415b852b2..6d9c2f92b214e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx @@ -55,7 +55,7 @@ export const AllRulesUtilityBar = React.memo( isBulkActionInProgress, hasDisabledActions, }) => { - const handleGetBuIktemsPopoverContent = useCallback( + const handleGetBulkItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { if (onGetBulkItemsPopoverContent != null) { return ( @@ -141,7 +141,7 @@ export const AllRulesUtilityBar = React.memo( iconSide="right" iconType="arrowDown" popoverPanelPaddingSize="none" - popoverContent={handleGetBuIktemsPopoverContent} + popoverContent={handleGetBulkItemsPopoverContent} > {i18n.BATCH_ACTIONS} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index ced24a576eea7..b3cf1ded717fa 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -27,7 +27,7 @@ jest.mock('react-router-dom', () => { }); jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/use_find_rules'); +jest.mock('../../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('../../../../../common/hooks/use_app_toasts'); 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 9ad0dfd70e336..464c4e58b88d2 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 @@ -227,8 +227,6 @@ const RuleDetailsPageComponent: React.FC = ({ const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; - // This is used to re-trigger api rule status when user de/activate rule - const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); const [pageTabs, setTabs] = useState(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = @@ -514,14 +512,9 @@ const RuleDetailsPageComponent: React.FC = ({ [dispatch] ); - const handleOnChangeEnabledRule = useCallback( - (enabled: boolean) => { - if (ruleEnabled == null || enabled !== ruleEnabled) { - setRuleEnabled(enabled); - } - }, - [ruleEnabled, setRuleEnabled] - ); + const handleOnChangeEnabledRule = useCallback((enabled: boolean) => { + setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule)); + }, []); const goToEditRule = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index c9aee723d599f..4532982032772 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -18,7 +18,7 @@ import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/use_find_rules'); +jest.mock('../../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 4de35119f80aa..4f40f1c86c697 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -27,8 +27,9 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('../../../containers/detection_engine/rules/rules_table/rules_table_context'); +jest.mock('../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); jest.mock('../../../containers/detection_engine/lists/use_lists_config'); +jest.mock('../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 65837879862af..e49e6801a1d8a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; @@ -37,16 +37,16 @@ import { MlJobCompatibilityCallout } from '../../../components/callouts/ml_job_c import { MissingPrivilegesCallOut } from '../../../components/callouts/missing_privileges_callout'; import { APP_UI_ID } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; -import { RulesTableContextProvider } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableContextProvider } from './all/rules_table/rules_table_context'; import { HeaderPage } from '../../../../common/components/header_page'; - -type Func = () => Promise; +import { useInvalidateRules } from '../../../containers/detection_engine/rules/use_find_rules_query'; +import { useBoolState } from '../../../../common/hooks/use_bool_state'; const RulesPageComponent: React.FC = () => { - const [showImportModal, setShowImportModal] = useState(false); - const [showValueListsModal, setShowValueListsModal] = useState(false); - const refreshRulesData = useRef(null); + const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); + const [isValueListModalVisible, showValueListModal, hideValueListModal] = useBoolState(); const { navigateToApp } = useKibana().services.application; + const invalidateRules = useInvalidateRules(); const [ { @@ -98,18 +98,12 @@ const RulesPageComponent: React.FC = () => { ); const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const handleRefreshRules = useCallback(async () => { - if (refreshRulesData.current != null) { - await refreshRulesData.current(); - } - }, [refreshRulesData]); - const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); - return handleRefreshRules(); + invalidateRules(); } - }, [createPrePackagedRules, handleRefreshRules]); + }, [createPrePackagedRules, invalidateRules]); const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { @@ -119,10 +113,6 @@ const RulesPageComponent: React.FC = () => { } }, [refetchPrePackagedRulesStatus]); - const handleSetRefreshRulesData = useCallback((refreshRule: Func) => { - refreshRulesData.current = refreshRule; - }, []); - const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -169,20 +159,17 @@ const RulesPageComponent: React.FC = () => { - setShowValueListsModal(false)} - /> + setShowImportModal(false)} + closeModal={hideImportModal} description={i18n.SELECT_RULE} errorMessage={i18n.IMPORT_FAILED} failedDetailed={i18n.IMPORT_FAILED_DETAILED} - importComplete={handleRefreshRules} + importComplete={invalidateRules} importData={importRules} successMessage={i18n.SUCCESSFULLY_IMPORTED_RULES} - showModal={showImportModal} + showModal={isImportModalVisible} submitBtnText={i18n.IMPORT_RULE_BTN_TITLE} subtitle={i18n.INITIAL_PROMPT_TEXT} title={i18n.IMPORT_RULE} @@ -207,7 +194,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="open-value-lists-modal-button" iconType="importAction" isDisabled={!canWriteListsIndex || loading} - onClick={() => setShowValueListsModal(true)} + onClick={showValueListModal} > {i18n.UPLOAD_VALUE_LISTS}
@@ -218,9 +205,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="rules-import-modal-button" iconType="importAction" isDisabled={!userHasPermissions(canUserCRUD) || loading} - onClick={() => { - setShowImportModal(true); - }} + onClick={showImportModal} > {i18n.IMPORT_RULE}
@@ -259,7 +244,6 @@ const RulesPageComponent: React.FC = () => { rulesInstalled={rulesInstalled} rulesNotInstalled={rulesNotInstalled} rulesNotUpdated={rulesNotUpdated} - setRefreshRulesData={handleSetRefreshRulesData} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 24d842eb930a8..1de060c16a97a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -247,7 +247,7 @@ export const BULK_EDIT_ERROR_TOAST_TITLE = i18n.translate( } ); -export const BULK_EDIT_ERROR_TOAST_DESCIRPTION = (rulesCount: number) => +export const BULK_EDIT_ERROR_TOAST_DESCRIPTION = (rulesCount: number) => i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription', { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts new file mode 100644 index 0000000000000..89cf2f0cbd276 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.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 { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { EVENT_FILTER_LIST_DEFINITION } from '../constants'; + +/** + * Event filters Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class EventFiltersApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super(http, ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_DEFINITION); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance(http, ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_DEFINITION); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts new file mode 100644 index 0000000000000..7e711732fa6f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from './constants'; + +/** + * Host isolation exceptions Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class HostIsolationExceptionsApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super( + http, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION + ); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION + ); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts new file mode 100644 index 0000000000000..4d1f0170b4117 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.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 { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants'; + +/** + * Trusted apps Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class TrustedAppsApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super(http, ENDPOINT_TRUSTED_APPS_LIST_ID, TRUSTED_APPS_EXCEPTION_LIST_DEFINITION); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_TRUSTED_APPS_LIST_ID, + TRUSTED_APPS_EXCEPTION_LIST_DEFINITION + ); + } +} 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 new file mode 100644 index 0000000000000..c1e3a2f76064f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart, HttpSetup } from 'kibana/public'; +import { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { ExceptionsListApiClient } from './exceptions_list_api_client'; + +const getFakeListId: () => string = () => 'FAKE_LIST_ID'; +const getFakeListDefinition: () => CreateExceptionListSchema = () => ({ + name: 'FAKE_LIST_NAME', + namespace_type: 'agnostic', + description: 'FAKE_LIST_DESCRIPTION', + list_id: getFakeListId(), + type: 'endpoint', +}); +const getQueryParams = () => ({ + page: 1, + perPage: 10, + filter: 'this is a KQL filter', + sortField: 'id', + sortOrder: 'asc', +}); + +describe('Exceptions List Api Client', () => { + let fakeCoreStart: jest.Mocked; + let fakeHttpServices: jest.Mocked; + let getInstance: () => ExceptionsListApiClient; + + // Initialize mocks once as the ExceptionsListApiClient is a singleton + beforeAll(() => { + fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); + fakeHttpServices = fakeCoreStart.http as jest.Mocked; + }); + + // Clear every function mock on each execution + beforeEach(() => { + fakeHttpServices.post.mockClear(); + fakeHttpServices.get.mockClear(); + fakeHttpServices.put.mockClear(); + fakeHttpServices.delete.mockClear(); + getInstance = () => + ExceptionsListApiClient.getInstance( + fakeHttpServices, + getFakeListId(), + getFakeListDefinition() + ); + }); + + describe('Wen getting an instance', () => { + /** + * ATENTION: Skipping or modifying this test may cause the other test fails because it's creating the initial Singleton instance. + * If you want to run tests individually, add this one to the execution with the .only method + */ + it('New instance is created the first time and the create list method is called', () => { + const exceptionsListApiClientInstance = getInstance(); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.post).toHaveBeenCalledWith(EXCEPTION_LIST_URL, { + body: JSON.stringify(getFakeListDefinition()), + }); + expect(exceptionsListApiClientInstance).toBeDefined(); + }); + + it('No new instance is created the second time and the creat list method is not called', () => { + const exceptionsListApiClientInstance = getInstance(); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(0); + expect(exceptionsListApiClientInstance).toBeDefined(); + }); + + it('Creating three instances from the same listId only creates the list one time', () => { + const newFakeListId = 'fakeListIdV2'; + const exceptionsListApiClientInstanceV1 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + const exceptionsListApiClientInstanceV2 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + const exceptionsListApiClientInstanceV3 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(exceptionsListApiClientInstanceV1).toBeDefined(); + expect(exceptionsListApiClientInstanceV2).toBeDefined(); + expect(exceptionsListApiClientInstanceV3).toBeDefined(); + }); + }); + + describe('Wen using public methods', () => { + it('Find method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + await exceptionsListApiClientInstance.find(getQueryParams()); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + const expectedQueryParams = getQueryParams(); + expect(fakeHttpServices.get).toHaveBeenCalledWith(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page: expectedQueryParams.page, + per_page: expectedQueryParams.perPage, + filter: expectedQueryParams.filter, + sort_field: expectedQueryParams.sortField, + sort_order: expectedQueryParams.sortOrder, + namespace_type: ['agnostic'], + list_id: [getFakeListId()], + }, + }); + }); + + it('Get method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeItemId = 'fakeId'; + + await exceptionsListApiClientInstance.get(fakeItemId); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.get).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + query: { + id: fakeItemId, + namespace_type: 'agnostic', + }, + }); + }); + + it('Create method calls http.post with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = { + ...new ExceptionsListItemGenerator('seed').generate(), + list_id: getFakeListId(), + }; + await exceptionsListApiClientInstance.create(exceptionItem); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.post).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exceptionItem), + }); + }); + + it('Throws when create method has wrong listId', async () => { + const wrongListId = 'wrong'; + const expectedError = new Error( + `The list id you are using is not valid, expected [${getFakeListId()}] list id but received [${wrongListId}] list id` + ); + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = new ExceptionsListItemGenerator('seed').generate(); + try { + await exceptionsListApiClientInstance.create({ ...exceptionItem, list_id: wrongListId }); + } catch (err) { + expect(err).toEqual(expectedError); + } + }); + + it('Update method calls http.put with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = new ExceptionsListItemGenerator('seed').generate(); + await exceptionsListApiClientInstance.update(exceptionItem); + + expect(fakeHttpServices.put).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.put).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(ExceptionsListApiClient.cleanExceptionsBeforeUpdate(exceptionItem)), + }); + }); + + it('Delete method calls http.delete with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeItemId = 'fakeId'; + + await exceptionsListApiClientInstance.delete(fakeItemId); + + expect(fakeHttpServices.delete).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.delete).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + query: { + id: fakeItemId, + namespace_type: 'agnostic', + }, + }); + }); + + it('Summary method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeQklFilter = 'KQL filter'; + + await exceptionsListApiClientInstance.summary(fakeQklFilter); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.get).toHaveBeenCalledWith(`${EXCEPTION_LIST_URL}/summary`, { + query: { + filter: fakeQklFilter, + namespace_type: 'agnostic', + }, + }); + }); + }); +}); 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 new file mode 100644 index 0000000000000..226d70ca30797 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, + CreateExceptionListSchema, + ExceptionListItemSchema, + ExceptionListSummarySchema, + FoundExceptionListItemSchema, + ListId, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; + +/** + * A generic class to be used for each artifact type. + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class ExceptionsListApiClient { + private static instance: Map = new Map(); + private static wasListCreated: Map> = new Map(); + private ensureListExists: Promise; + + constructor( + private readonly http: HttpStart, + private readonly listId: ListId, + private readonly listDefinition: CreateExceptionListSchema + ) { + this.ensureListExists = this.createExceptionList(); + } + + /** + * PrivateStatic method that creates the list and don't throw if list already exists. + * This method is being used when initializing an instance only once. + */ + private async createExceptionList(): Promise { + if (ExceptionsListApiClient.wasListCreated.has(this.listId)) { + return ExceptionsListApiClient.wasListCreated.get(this.listId); + } + ExceptionsListApiClient.wasListCreated.set( + this.listId, + new Promise(async (resolve, reject) => { + try { + await this.http.post(EXCEPTION_LIST_URL, { + body: JSON.stringify({ ...this.listDefinition, list_id: this.listId }), + }); + + resolve(); + } catch (err) { + // Ignore 409 errors. List already created + if (err.response?.status !== 409) { + reject(err); + } + + resolve(); + } + }) + ); + + ExceptionsListApiClient.wasListCreated.get(this.listId)?.catch(() => { + ExceptionsListApiClient.wasListCreated.delete(this.listId); + }); + + return ExceptionsListApiClient.wasListCreated.get(this.listId); + } + + /** + * Private method that throws an error when some of the checks to ensure the instance + * we are using is the right one fail + */ + private checkIfIsUsingTheRightInstance(listId: ListId): void { + if (listId !== this.listId) { + throw new Error( + `The list id you are using is not valid, expected [${this.listId}] list id but received [${listId}] list id` + ); + } + } + + /** + * Static method to get a fresh or existing instance. + * It will ensure we only check and create the list once. + */ + public static getInstance( + http: HttpStart, + listId: string, + listDefinition: CreateExceptionListSchema + ): ExceptionsListApiClient { + if (!ExceptionsListApiClient.instance.has(listId)) { + ExceptionsListApiClient.instance.set( + listId, + new ExceptionsListApiClient(http, listId, listDefinition) + ); + } + const currentInstance = ExceptionsListApiClient.instance.get(listId); + if (currentInstance) { + return currentInstance; + } else { + return new ExceptionsListApiClient(http, listId, listDefinition); + } + } + + /** + * Static method to clean an exception item before sending it to update. + */ + public static cleanExceptionsBeforeUpdate( + exception: UpdateExceptionListItemSchema + ): UpdateExceptionListItemSchema { + const exceptionToUpdateCleaned = { ...exception }; + // Clean unnecessary fields for update action + [ + 'created_at', + 'created_by', + 'created_at', + 'created_by', + 'list_id', + 'tie_breaker_id', + 'updated_at', + 'updated_by', + ].forEach((field) => { + delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; + }); + + exceptionToUpdateCleaned.comments = exceptionToUpdateCleaned.comments?.map((comment) => ({ + comment: comment.comment, + id: comment.id, + })); + + return exceptionToUpdateCleaned as UpdateExceptionListItemSchema; + } + + /** + * Returns a list of items with pagination params. + * It accepts the allowed filtering, sorting and pagination options as param. + */ + async find({ + perPage = MANAGEMENT_DEFAULT_PAGE_SIZE, + page = MANAGEMENT_DEFAULT_PAGE + 1, + sortField, + sortOrder, + filter, + }: Partial<{ + page: number; + perPage: number; + sortField: string; + sortOrder: string; + filter: string; + }> = {}): Promise { + await this.ensureListExists; + return this.http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + list_id: [this.listId], + namespace_type: ['agnostic'], + filter, + }, + }); + } + + /** + * Returns an item filtered by id + * It requires an id in order to get the desired item + */ + async get(id: string): Promise { + await this.ensureListExists; + return this.http.get(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); + } + + /** + * It creates an item and returns the created one. + * It requires a CreateExceptionListItemSchema object. + */ + async create(exception: CreateExceptionListItemSchema): Promise { + await this.ensureListExists; + this.checkIfIsUsingTheRightInstance(exception.list_id); + return this.http.post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); + } + + /** + * It updates an existing item and returns the updated one. + * It requires a UpdateExceptionListItemSchema object. + */ + async update(exception: UpdateExceptionListItemSchema): Promise { + await this.ensureListExists; + return this.http.put(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(ExceptionsListApiClient.cleanExceptionsBeforeUpdate(exception)), + }); + } + + /** + * It deletes an existing item. + * It requires a valid item id. + */ + async delete(id: string): Promise { + await this.ensureListExists; + return this.http.delete(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); + } + + /** + * It returns a summary of the current list_id + * It accepts a filter param to filter the summary results using KQL filtering. + */ + async summary(filter?: string): Promise { + await this.ensureListExists; + return this.http.get(`${EXCEPTION_LIST_URL}/summary`, { + query: { + filter, + namespace_type: 'agnostic', + }, + }); + } +} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts index 555ae7544180b..5bc2087dc63ab 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts @@ -16,13 +16,15 @@ export const DASHBOARD_REQUEST_BODY = { }; export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; + const { + dashboard, + savedObjects: { client: savedObjectsClient }, + } = useKibana().services; const [buttonHref, setButtonHref] = useState(); useEffect(() => { - if (createDashboardUrl && savedObjectsClient) { + if (dashboard?.locator && savedObjectsClient) { savedObjectsClient.find(DASHBOARD_REQUEST_BODY).then( async (DashboardsSO?: { savedObjects?: Array<{ @@ -31,7 +33,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrl = await createDashboardUrl({ + const dashboardUrl = await dashboard?.locator?.getUrl({ dashboardId: DashboardsSO.savedObjects[0].id, timeRange: { to, @@ -43,7 +45,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { } ); } - }, [createDashboardUrl, from, savedObjectsClient, to]); + }, [dashboard, from, savedObjectsClient, to]); return { buttonHref, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx index 002dc18227f6d..5b8bf180da1f8 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx @@ -14,40 +14,48 @@ export const useRiskyHostsDashboardLinks = ( from: string, listItems: LinkPanelListItem[] ) => { - const createDashboardUrl = useKibana().services.dashboard?.locator?.getLocation; + const { dashboard } = useKibana().services; + const dashboardId = useRiskyHostsDashboardId(); const [listItemsWithLinks, setListItemsWithLinks] = useState([]); useEffect(() => { let cancelled = false; const createLinks = async () => { - if (createDashboardUrl && dashboardId) { + if (dashboard?.locator && dashboardId) { const dashboardUrls = await Promise.all( - listItems.map((listItem) => - createDashboardUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }) + listItems.reduce( + (acc: Array>, listItem) => + dashboard && dashboard.locator + ? [ + ...acc, + dashboard.locator.getUrl({ + dashboardId, + timeRange: { + to, + from, + }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { match_phrase: { 'host.name': listItem.title } }, + }, + ], + }), + ] + : acc, + [] ) ); - if (!cancelled) { + if (!cancelled && dashboardUrls.length) { setListItemsWithLinks( listItems.map((item, i) => ({ ...item, - path: dashboardUrls[i] as unknown as string, + path: dashboardUrls[i], })) ); } @@ -59,7 +67,7 @@ export const useRiskyHostsDashboardLinks = ( return () => { cancelled = true; }; - }, [createDashboardUrl, dashboardId, from, listItems, to]); + }, [dashboard, dashboardId, from, listItems, to]); return { listItemsWithLinks }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts index 1d97b7a39779a..ae253dfa3438c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -136,9 +136,9 @@ describe('legacyRules_notification_alert_type', () => { ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -163,9 +163,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -192,9 +192,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -204,7 +204,7 @@ describe('legacyRules_notification_alert_type', () => { ); }); - it('should not call alertInstanceFactory if signalsCount was 0', async () => { + it('should not call alertFactory.create if signalsCount was 0', async () => { const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', @@ -218,7 +218,7 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should call scheduleActions if signalsCount was greater than 0', async () => { @@ -237,9 +237,9 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( expect.objectContaining({ signals_count: 100 }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts index 6a5a9478681f3..62d187bd3ea0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts @@ -119,7 +119,7 @@ export const legacyRulesNotificationAlertType = ({ ); if (signalsCount !== 0) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts index b40f6c6f8a72d..eebda81fd63f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts @@ -54,7 +54,7 @@ describe('schedule_notification_actions', () => { }; it('Should schedule actions with unflatted and legacy context', () => { - const alertInstance = alertServices.alertInstanceFactory(alertId); + const alertInstance = alertServices.alertFactory.create(alertId); const signals = [sampleThresholdAlert._source, sampleThresholdAlert._source]; scheduleNotificationActions({ alertInstance, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 9b20b031eea0f..394e431203a24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -6,7 +6,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import { RuleParams } from '../schemas/rule_schemas'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; @@ -46,7 +46,7 @@ const formatAlertsForNotificationActions = (alerts: unknown[]): unknown[] => { }; interface ScheduleNotificationActions { - alertInstance: AlertInstance; + alertInstance: Alert; signalsCount: number; resultsLink: string; ruleParams: NotificationRuleTypeParams; @@ -59,7 +59,7 @@ export const scheduleNotificationActions = ({ resultsLink = '', ruleParams, signals, -}: ScheduleNotificationActions): AlertInstance => +}: ScheduleNotificationActions): Alert => alertInstance .replaceState({ signals_count: signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 964df3c91eb08..b5dffa7b34c14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -82,7 +82,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -107,7 +107,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -137,7 +137,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -166,7 +166,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -197,7 +197,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -235,7 +235,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -271,7 +271,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -313,7 +313,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -375,7 +375,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -435,7 +435,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -497,7 +497,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -559,7 +559,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index cab590b3e2513..2399962ad281e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, SavedObject, Logger } from 'src/core/server'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; import { deconflictSignalsAndResults, getNotificationResultsLink } from '../notifications/utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; @@ -26,7 +26,7 @@ interface ScheduleThrottledNotificationActionsOptions { outputIndex: RuleParams['outputIndex']; ruleId: RuleParams['ruleId']; esClient: ElasticsearchClient; - alertInstance: AlertInstance; + alertInstance: Alert; notificationRuleParams: NotificationRuleTypeParams; signals: unknown[]; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 369bff6f615fc..0ac11b80c2a65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -30,11 +30,10 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { deleteRules } from '../../rules/delete_rules'; import { duplicateRule } from '../../rules/duplicate_rule'; -import { enableRule } from '../../rules/enable_rule'; import { findRules } from '../../rules/find_rules'; import { readRules } from '../../rules/read_rules'; import { patchRules } from '../../rules/patch_rules'; -import { appplyBulkActionEditToRule } from '../../rules/bulk_action_edit'; +import { applyBulkActionEditToRule } from '../../rules/bulk_action_edit'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { buildSiemResponse } from '../utils'; @@ -297,10 +296,7 @@ export const performBulkActionRoute = ( async (rule) => { if (!rule.enabled) { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - await enableRule({ - rule, - rulesClient, - }); + await rulesClient.enable({ id: rule.id }); } }, abortController.signal @@ -375,7 +371,7 @@ export const performBulkActionRoute = ( throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); const editedRule = body[BulkAction.edit].reduce( - (acc, action) => appplyBulkActionEditToRule(acc, action), + (acc, action) => applyBulkActionEditToRule(acc, action), rule ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index ed0aa04b6a08c..8d89bc66b2041 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -34,7 +34,7 @@ import { } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ExecutorType } from '../../../../../../alerting/server/types'; -import { AlertInstance } from '../../../../../../alerting/server'; +import { Alert } from '../../../../../../alerting/server'; import { ConfigType } from '../../../../config'; import { alertInstanceFactoryStub } from '../../signals/preview/alert_instance_factory_stub'; import { CreateRuleOptions, CreateSecurityRuleTypeWrapperProps } from '../../rule_types/types'; @@ -140,12 +140,14 @@ export const previewRulesRoute = async ( ruleTypeName: string, params: TParams, shouldWriteAlerts: () => boolean, - alertInstanceFactory: ( - id: string - ) => Pick< - AlertInstance, - 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' - > + alertFactory: { + create: ( + id: string + ) => Pick< + Alert, + 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' + >; + } ) => { let statePreview = runState as TState; @@ -178,7 +180,7 @@ export const previewRulesRoute = async ( services: { shouldWriteAlerts, shouldStopExecution: () => false, - alertInstanceFactory, + alertFactory, // Just use es client always for preview search: context.core.elasticsearch.client, savedObjectsClient: context.core.savedObjects.client, @@ -223,7 +225,7 @@ export const previewRulesRoute = async ( queryAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threshold': @@ -236,7 +238,7 @@ export const previewRulesRoute = async ( thresholdAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threat_match': @@ -249,7 +251,7 @@ export const previewRulesRoute = async ( threatMatchAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'eql': @@ -260,7 +262,7 @@ export const previewRulesRoute = async ( eqlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'machine_learning': @@ -271,7 +273,7 @@ export const previewRulesRoute = async ( mlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 3774930204ae6..3d96e3bb77907 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -76,7 +76,7 @@ export const createRuleTypeMocks = ( search: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: mockSavedObjectsClient, scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, findAlerts: jest.fn(), // TODO: does this stay? alertWithPersistence: jest.fn(), logger: loggerMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index e2fc5442d4c80..00244832d0191 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -315,7 +315,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: alertId, @@ -329,7 +329,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = logger, }); } else if (createdSignalsCount) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount: createdSignalsCount, @@ -371,7 +371,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, @@ -403,7 +403,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts index 3493025749f98..35c91ba398f6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts @@ -13,11 +13,13 @@ export const filterSource = (doc: SignalSourceHit): Partial => { const docSource = doc._source ?? {}; const { event, + signal, threshold_result: siemSignalsThresholdResult, [ALERT_THRESHOLD_RESULT]: alertThresholdResult, ...filteredSource } = docSource || { event: null, + signal: null, threshold_result: null, [ALERT_THRESHOLD_RESULT]: null, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts index db6ef37cade36..783e710cb7d4d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts @@ -8,7 +8,7 @@ import { addItemsToArray, deleteItemsFromArray, - appplyBulkActionEditToRule, + applyBulkActionEditToRule, } from './bulk_action_edit'; import { BulkActionEditType } from '../../../../common/detection_engine/schemas/common/schemas'; import { RuleAlertType } from './types'; @@ -41,21 +41,21 @@ describe('bulk_action_edit', () => { }); }); - describe('appplyBulkActionEditToRule', () => { + describe('applyBulkActionEditToRule', () => { const ruleMock = { tags: ['tag1', 'tag2'], params: { index: ['initial-index-*'] }, }; describe('tags', () => { test('should add new tags to rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.add_tags, value: ['new_tag'], }); expect(editedRule.tags).toEqual(['tag1', 'tag2', 'new_tag']); }); test('should remove tag from rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.delete_tags, value: ['tag1'], }); @@ -63,7 +63,7 @@ describe('bulk_action_edit', () => { }); test('should rewrite tags in rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_tags, value: ['tag_r_1', 'tag_r_2'], }); @@ -73,14 +73,14 @@ describe('bulk_action_edit', () => { describe('index_patterns', () => { test('should add new index pattern to rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }); expect(editedRule.params).toHaveProperty('index', ['initial-index-*', 'my-index-*']); }); test('should remove index pattern from rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.delete_index_patterns, value: ['initial-index-*'], }); @@ -88,7 +88,7 @@ describe('bulk_action_edit', () => { }); test('should rewrite index pattern in rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_index_patterns, value: ['index'], }); @@ -96,7 +96,7 @@ describe('bulk_action_edit', () => { }); test('should not add new index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }); @@ -104,7 +104,7 @@ describe('bulk_action_edit', () => { }); test('should not remove index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.delete_index_patterns, value: ['initial-index-*'], }); @@ -112,7 +112,7 @@ describe('bulk_action_edit', () => { }); test('should not set index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.set_index_patterns, value: ['index-*'], }); @@ -122,7 +122,7 @@ describe('bulk_action_edit', () => { describe('timeline', () => { test('should set timeline', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_timeline, value: { timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts index 0f56fd86be8ed..eab37e6474f32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts @@ -20,7 +20,7 @@ export const deleteItemsFromArray = (arr: T[], items: T[]): T[] => { return arr.filter((item) => !itemsSet.has(item)); }; -export const appplyBulkActionEditToRule = ( +export const applyBulkActionEditToRule = ( existingRule: RuleAlertType, action: BulkActionEditPayload ): RuleAlertType => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts deleted file mode 100644 index fb9aeefc06125..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.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 { SanitizedAlert } from '../../../../../alerting/common'; -import { RulesClient } from '../../../../../alerting/server'; -import { RuleParams } from '../schemas/rule_schemas'; - -interface EnableRuleArgs { - rule: SanitizedAlert; - rulesClient: RulesClient; -} - -/** - * Enables the rule - * - * @param rule - rule to enable - * @param rulesClient - Alerts client - */ -export const enableRule = async ({ rule, rulesClient }: EnableRuleArgs) => { - await rulesClient.enable({ id: rule.id }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index a10247005c826..b862dca6a022a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -15,7 +15,6 @@ import { } from '../../../../common/detection_engine/utils'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { addTags } from './add_tags'; -import { enableRule } from './enable_rule'; import { PatchRulesOptions } from './types'; import { calculateInterval, @@ -219,7 +218,7 @@ export const patchRules = async ({ if (rule.enabled && enabled === false) { await rulesClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { - await enableRule({ rule, rulesClient }); + await rulesClient.enable({ id: rule.id }); } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 62c59bc6a698f..6c13955aaab58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -15,7 +15,6 @@ import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; -import { enableRule } from './enable_rule'; import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils'; class UpdateError extends Error { @@ -102,7 +101,7 @@ export const updateRules = async ({ if (existingRule.enabled && enabled === false) { await rulesClient.disable({ id: existingRule.id }); } else if (!existingRule.enabled && enabled === true) { - await enableRule({ rule: existingRule, rulesClient }); + await rulesClient.enable({ id: existingRule.id }); } return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts index d09314312c78d..7cc709bbe8994 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts @@ -12,7 +12,7 @@ import { AlertTypeState, } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertInstance } from '../../../../../../alerting/server/alert_instance'; +import { Alert } from '../../../../../../alerting/server/alert'; export const alertInstanceFactoryStub = < TParams extends RuleParams, @@ -27,13 +27,13 @@ export const alertInstanceFactoryStub = < return {} as unknown as TInstanceState; }, replaceState(state: TInstanceState) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, scheduleActions(actionGroup: TActionGroupIds, alertcontext: TInstanceContext) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); @@ -43,7 +43,7 @@ export const alertInstanceFactoryStub = < subgroup: string, alertcontext: TInstanceContext ) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index c0d05c44201fb..307496e2be391 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -178,7 +178,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); expect(result).toMatchInlineSnapshot(` Object { @@ -257,8 +257,8 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -328,7 +328,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward latestTimestamp: new Date(previousTimestamp).toISOString(), @@ -410,7 +410,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -488,7 +488,7 @@ describe('alertType', () => { }; const result = await alertType.executor(executorOptions); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -518,7 +518,7 @@ describe('alertType', () => { state: result as EsQueryAlertState, }); const existingInstance: AlertInstanceMock = - alertServices.alertInstanceFactory.mock.results[1].value; + alertServices.alertFactory.create.mock.results[1].value; expect(existingInstance.replaceState).toHaveBeenCalledWith({ latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), dateStart: expect.any(String), @@ -601,7 +601,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -685,7 +685,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 9dca9e9c3fc61..6a1fcc6a3d7bb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -160,7 +160,7 @@ export function getAlertType(logger: Logger): RuleType< > ) { const { alertId, name, services, params, state } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const previousTimestamp = state.latestTimestamp; const abortableEsClient = search.asCurrentUser; @@ -255,7 +255,7 @@ export function getAlertType(logger: Logger): RuleType< }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(ConditionMetAlertInstanceId); + const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); alertInstance // store the params we would need to recreate the query that led to this alert instance .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index ecd08d3dc432f..aca79e29cd3e5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -87,11 +87,11 @@ export function transformResults( export function getActiveEntriesAndGenerateAlerts( prevLocationMap: Map, currLocationMap: Map, - alertInstanceFactory: AlertServices< + alertFactory: AlertServices< GeoContainmentInstanceState, GeoContainmentInstanceContext, typeof ActionGroupId - >['alertInstanceFactory'], + >['alertFactory'], shapesIdsNamesMap: Record, currIntervalEndTime: Date ) { @@ -113,7 +113,7 @@ export function getActiveEntriesAndGenerateAlerts( }; const alertInstanceId = `${entityName}-${context.containingBoundaryName}`; if (shapeLocationId !== OTHER_CATEGORY) { - alertInstanceFactory(alertInstanceId).scheduleActions(ActionGroupId, context); + alertFactory.create(alertInstanceId).scheduleActions(ActionGroupId, context); } }); @@ -189,7 +189,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMap, currLocationMap, - services.alertInstanceFactory, + services.alertFactory, shapesIdsNamesMap, currIntervalEndTime ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 8b78441d174b2..dc633e298490c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -20,9 +20,9 @@ import { OTHER_CATEGORY } from '../es_query_builder'; import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; import type { GeoContainmentParams } from '../alert_type'; -const alertInstanceFactory = - (contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => { - const alertInstance = alertsMock.createAlertInstanceFactory< +const alertFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => ({ + create: (instanceId: string) => { + const alertInstance = alertsMock.createAlertFactory.create< GeoContainmentInstanceState, GeoContainmentInstanceContext >(); @@ -39,7 +39,8 @@ const alertInstanceFactory = } ); return alertInstance; - }; + }, +}); describe('geo_containment', () => { describe('transformResults', () => { @@ -253,7 +254,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -278,7 +279,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -317,7 +318,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithNonIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -340,7 +341,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -373,7 +374,7 @@ describe('geo_containment', () => { getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithThreeMore, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -410,7 +411,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -442,7 +443,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -514,7 +515,7 @@ describe('geo_containment', () => { const alertServicesWithSearchMock: AlertServicesMock = { ...alertsMock.createAlertServices(), // @ts-ignore - alertInstanceFactory: alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory: alertFactory(contextKeys, testAlertActionArr), scopedClusterClient: { asCurrentUser: { // @ts-ignore @@ -538,6 +539,7 @@ describe('geo_containment', () => { it('should query for shapes if state does not contain shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -557,6 +559,7 @@ describe('geo_containment', () => { it('should not query for shapes if state contains shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -575,6 +578,7 @@ describe('geo_containment', () => { it('should carry through shapes filters in state to next call unmodified', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -610,6 +614,7 @@ describe('geo_containment', () => { ], }; const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index f6b1c4a3a3b0a..e55ce6e3a3aba 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -203,7 +203,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith('all documents'); + expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); }); it('should ensure a null result does not fire actions', async () => { @@ -269,7 +269,7 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should ensure an undefined result does not fire actions', async () => { @@ -335,6 +335,6 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index e31744e770462..0eb2810626ac3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -134,7 +134,7 @@ export function getAlertType( options: AlertExecutorOptions ) { const { alertId, name, services, params } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { @@ -208,7 +208,7 @@ export function getAlertType( conditions: humanFn, }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(instanceId); + const alertInstance = alertFactory.create(instanceId); alertInstance.scheduleActions(ActionGroupId, actionContext); logger.debug(`scheduled actionGroup: ${JSON.stringify(actionContext)}`); } diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts index f33afc2b54285..8b9897a0e9436 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -100,7 +100,7 @@ export function getTransformHealthRuleType(): RuleType< isExportable: true, async executor(options) { const { - services: { scopedClusterClient, alertInstanceFactory }, + services: { scopedClusterClient, alertFactory }, params, } = options; @@ -112,7 +112,7 @@ export function getTransformHealthRuleType(): RuleType< if (executionResult.length > 0) { executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = alertInstanceFactory(alertInstanceName); + const alertInstance = alertFactory.create(alertInstanceName); alertInstance.scheduleActions(TRANSFORM_ISSUE, context); }); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 90d43e476e2ff..06542cbb3a1a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -585,7 +585,8 @@ export const AlertForm = ({ position="right" type="questionInCircle" content={i18n.translate('xpack.triggersActionsUI.sections.alertForm.checkWithTooltip', { - defaultMessage: 'Define how often to evaluate the condition.', + defaultMessage: + 'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.', })} /> diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index 6cef41347bbf6..26e3d726a10c0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -29,6 +29,26 @@ export const ServiceLocationCodec = t.interface({ url: t.string, }); +export const ServiceLocationErrors = t.array( + t.intersection([ + t.interface({ + locationId: t.string, + error: t.interface({ + reason: t.string, + status: t.number, + }), + }), + t.partial({ + failed_monitors: t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + }), + ]) +); + export const ServiceLocationsCodec = t.array(ServiceLocationCodec); export const isServiceLocationInvalid = (location: ServiceLocation) => @@ -42,3 +62,4 @@ export type ManifestLocation = t.TypeOf; export type ServiceLocation = t.TypeOf; export type ServiceLocations = t.TypeOf; export type ServiceLocationsApiResponse = t.TypeOf; +export type ServiceLocationErrors = t.TypeOf; diff --git a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts index 4045aa952506b..ec6b87bb0bf53 100644 --- a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts +++ b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts @@ -12,4 +12,6 @@ export interface MonitorIdParam { monitorId: string; } -export type SyntheticsMonitorSavedObject = SimpleSavedObject; +export type SyntheticsMonitorSavedObject = SimpleSavedObject & { + updated_at: string; +}; diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index ce197d574aa15..fe8a4960eac12 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,8 +7,8 @@ export * from './data_view_permissions'; export * from './uptime.journey'; -export * from './monitor_management.journey'; export * from './step_duration.journey'; export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; +export * from './monitor_management.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts index beb84a9a003a2..456d219adef05 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts @@ -12,7 +12,7 @@ * 2.0. */ -import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { journey, step, expect, after, before, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { byTestId } from './utils'; @@ -23,6 +23,11 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => await uptime.waitForLoadingToFinish(); }); + after(async () => { + await uptime.navigateToMonitorManagement(); + await uptime.deleteMonitor(); + }); + step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 057ce21ec5100..fd877708f2bce 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -88,7 +88,7 @@ export function monitorManagementPageProvider({ } else { await page.click('text=Save monitor'); } - return await this.findByTestSubj('uptimeAddMonitorSuccess'); + return await this.findByText('Monitor added successfully.'); }, async fillCodeEditor(value: string) { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 8c9dc7ffe6275..314347331b5b3 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -28,6 +29,10 @@ import { SyntheticsMonitor } from '../../../../common/runtime_types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TestRun } from '../test_now_mode/test_now_mode'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +import { kibanaService } from '../../../state/kibana_service'; + export interface ActionBarProps { monitor: SyntheticsMonitor; isValid: boolean; @@ -39,11 +44,11 @@ export interface ActionBarProps { export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: ActionBarProps) => { const { monitorId } = useParams<{ monitorId: string }>(); const { basePath } = useContext(UptimeSettingsContext); + const { locations } = useSelector(monitorManagementListSelector); const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false); const [isSaving, setIsSaving] = useState(false); - - const { notifications } = useKibana(); + const [isSuccessful, setIsSuccessful] = useState(false); const { data, status } = useFetcher(() => { if (!isSaving || !isValid) { @@ -55,6 +60,9 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti }); }, [monitor, monitorId, isValid, isSaving]); + const hasErrors = data && Object.keys(data).length; + const loading = status === FETCH_STATUS.LOADING; + const handleOnSave = useCallback(() => { if (onSave) { onSave(); @@ -75,23 +83,57 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti setIsSaving(false); } if (status === FETCH_STATUS.FAILURE) { - notifications.toasts.danger({ - title:

{MONITOR_FAILURE_LABEL}

, + kibanaService.toasts.addDanger({ + title: MONITOR_FAILURE_LABEL, toastLifeTimeMs: 3000, }); - } else if (status === FETCH_STATUS.SUCCESS) { - notifications.toasts.success({ - title: ( -

- {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL} -

- ), + } else if (status === FETCH_STATUS.SUCCESS && !hasErrors && !loading) { + kibanaService.toasts.addSuccess({ + title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, toastLifeTimeMs: 3000, }); + setIsSuccessful(true); + } else if (hasErrors && !loading) { + Object.values(data).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+

+ {status + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ + ), + toastLifeTimeMs: 30000, + }); + }); + setIsSuccessful(true); } - }, [data, status, notifications.toasts, isSaving, isValid, monitorId]); + }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); - return status === FETCH_STATUS.SUCCESS ? ( + return isSuccessful ? ( ) : ( @@ -191,7 +233,6 @@ const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( } ); -// TODO: Discuss error states with product const MONITOR_FAILURE_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.monitorFailureMessage', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx new file mode 100644 index 0000000000000..f217631bfe33d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { FETCH_STATUS } from '../../../../../observability/public'; +import { + DataStream, + HTTPFields, + ScheduleUnit, + SyntheticsMonitor, +} from '../../../../common/runtime_types'; +import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; +import * as kibana from '../../../state/kibana_service'; +import { ActionBar } from './action_bar'; +import { mockLocationsState } from '../mocks'; + +jest.mock('../../../state/kibana_service', () => ({ + ...jest.requireActual('../../../state/kibana_service'), + kibanaService: { + toasts: { + addWarning: jest.fn(), + }, + }, +})); + +const monitor: SyntheticsMonitor = { + name: 'test-monitor', + schedule: { + unit: ScheduleUnit.MINUTES, + number: '2', + }, + urls: 'https://elastic.co', + type: DataStream.HTTP, +} as unknown as HTTPFields; + +describe(' Service Errors', () => { + let useFetcher: jest.SpyInstance; + const toast = jest.fn(); + + beforeEach(() => { + useFetcher?.mockClear(); + useFetcher = spyOnUseFetcher({}); + }); + + it('Handles service errors', async () => { + jest.spyOn(kibana.kibanaService.toasts, 'addWarning').mockImplementation(toast); + useFetcher.mockReturnValue({ + data: [ + { locationId: 'us_central', error: { reason: 'Invalid config', status: 400 } }, + { locationId: 'us_central', error: { reason: 'Cannot schedule', status: 500 } }, + ], + status: FETCH_STATUS.SUCCESS, + refetch: () => {}, + }); + render(, { state: mockLocationsState }); + userEvent.click(screen.getByText('Save monitor')); + + await waitFor(() => { + expect(toast).toBeCalledTimes(2); + expect(toast).toBeCalledWith( + expect.objectContaining({ + title: 'Unable to sync monitor config', + toastLifeTimeMs: 30000, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts new file mode 100644 index 0000000000000..1ec4437601d57 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './locations'; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts new file mode 100644 index 0000000000000..b4f23bed097cb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.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. + */ +export const mockLocation = { + label: 'US Central', + id: 'us_central', + geo: { + lat: 1, + lon: 1, + }, + url: 'url', +}; + +export const mockLocationsState = { + monitorManagementList: { + locations: [mockLocation], + list: { + monitors: [], + perPage: 10, + page: 1, + total: 0, + }, + error: { + serviceLocations: null, + monitorList: null, + }, + loading: { + serviceLocations: false, + monitorList: false, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/state/api/monitor_management.ts b/x-pack/plugins/uptime/public/state/api/monitor_management.ts index ec2806907baa1..206ba07dc4c23 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_management.ts @@ -13,6 +13,7 @@ import { ServiceLocations, SyntheticsMonitor, ServiceLocationsApiResponseCodec, + ServiceLocationErrors, } from '../../../common/runtime_types'; import { SyntheticsMonitorSavedObject } from '../../../common/types'; import { apiService } from './utils'; @@ -23,7 +24,7 @@ export const setMonitor = async ({ }: { monitor: SyntheticsMonitor; id?: string; -}): Promise => { +}): Promise => { if (id) { return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); } else { diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index c6f5eae2e9d82..b9dfd61e91b70 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -52,6 +52,7 @@ export interface UptimeServerSetup { syntheticsService: SyntheticsService; kibanaVersion: string; logger: Logger; + uptimeEsClient: UptimeESClient; } export interface UptimeCorePluginsSetup { diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts index 8236af03de85c..e662160bef3e0 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -16,7 +16,7 @@ import { commonStateTranslations, tlsTranslations } from './translations'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { AlertInstanceContext } from '../../../../alerting/common'; -import { AlertInstance } from '../../../../alerting/server'; +import { Alert } from '../../../../alerting/server'; import { savedObjectsAdapter } from '../saved_objects/saved_objects'; import { createUptimeESClient } from '../lib'; @@ -28,7 +28,7 @@ import { export type ActionGroupIds = ActionGroupIdsOf; -type TLSAlertInstance = AlertInstance, AlertInstanceContext, ActionGroupIds>; +type TLSAlertInstance = Alert, AlertInstanceContext, ActionGroupIds>; interface TlsAlertState { count: number; @@ -113,10 +113,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s }, isExportable: true, minimumLicenseRequired: 'basic', - async executor({ - services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient }, - state, - }) { + async executor({ services: { alertFactory, scopedClusterClient, savedObjectsClient }, state }) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); const uptimeEsClient = createUptimeESClient({ @@ -156,7 +153,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s 'd' ) .valueOf(); - const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id); + const alertInstance: TLSAlertInstance = alertFactory.create(TLS_LEGACY.id); const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); alertInstance.replaceState({ ...updateState(state, foundCerts), diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts new file mode 100644 index 0000000000000..2e98b62ddee66 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { UptimeESClient } from '../lib'; +import { UptimeServerSetup } from '../adapters'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; +import { MonitorFields, Ping } from '../../../common/runtime_types'; + +export const hydrateSavedObjects = async ({ + monitors, + server, +}: { + monitors: SyntheticsMonitorSavedObject[]; + server: UptimeServerSetup; +}) => { + const missingUrlInfoIds: string[] = []; + + monitors + .filter((monitor) => monitor.attributes.type === 'browser') + .forEach(({ attributes, id }) => { + const monitor = attributes as MonitorFields; + if (!monitor || !monitor.urls) { + missingUrlInfoIds.push(id); + } + }); + + if (missingUrlInfoIds.length > 0 && server.uptimeEsClient) { + const esDocs: Ping[] = await fetchSampleMonitorDocuments( + server.uptimeEsClient, + missingUrlInfoIds + ); + const updatedObjects = monitors + .filter((monitor) => missingUrlInfoIds.includes(monitor.id)) + .map((monitor) => { + let url = ''; + esDocs.forEach((doc) => { + // to make sure the document is ingested after the latest update of the monitor + const diff = moment(monitor.updated_at).diff(moment(doc.timestamp), 'minutes'); + if (doc.config_id === monitor.id && doc.url?.full && diff > 1) { + url = doc.url?.full; + } + }); + if (url) { + return { ...monitor, attributes: { ...monitor.attributes, urls: url } }; + } + return monitor; + }); + await server.authSavedObjectsClient?.bulkUpdate(updatedObjects); + } +}; + +const fetchSampleMonitorDocuments = async (esClient: UptimeESClient, configIds: string[]) => { + const data = await esClient.search({ + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: 'now-15m', + lt: 'now', + }, + }, + }, + { + terms: { + config_id: configIds, + }, + }, + { + term: { + 'monitor.type': 'browser', + }, + }, + { + exists: { + field: 'summary', + }, + }, + { + exists: { + field: 'url.full', + }, + }, + ], + }, + }, + _source: ['url', 'config_id', '@timestamp'], + collapse: { + field: 'config_id', + }, + }, + }); + + return data.body.hits.hits.map( + ({ _source: doc }) => ({ ...(doc as any), timestamp: (doc as any)['@timestamp'] } as Ping) + ); +}; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 596a64b4d359a..1e82ef77e083b 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -11,7 +11,11 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '../../../../../../src/core/server'; -import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; +import { + MonitorFields, + ServiceLocations, + ServiceLocationErrors, +} from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; import { ServiceConfig } from '../../../common/config'; @@ -109,7 +113,7 @@ export class ServiceAPIClient { }); }; - const pushErrors: Array<{ locationId: string; error: Error }> = []; + const pushErrors: ServiceLocationErrors = []; const promises: Array> = []; @@ -128,7 +132,7 @@ export class ServiceAPIClient { ); }), catchError((err) => { - pushErrors.push({ locationId: id, error: err }); + pushErrors.push({ locationId: id, error: err.response?.data }); this.logger.error(err); // we don't want to throw an unhandled exception here return of(true); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 20f11fe3b8900..450ab324e7e48 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -31,6 +31,8 @@ import { SyntheticsMonitorWithId, } from '../../../common/runtime_types'; import { getServiceLocations } from './get_service_locations'; +import { hydrateSavedObjects } from './hydrate_saved_object'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE = 'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects'; @@ -280,6 +282,11 @@ export class SyntheticsService { namespaces: ['*'], }); + hydrateSavedObjects({ + monitors: findResult.saved_objects as unknown as SyntheticsMonitorSavedObject[], + server: this.server, + }); + return (findResult.saved_objects ?? []).map(({ attributes, id }) => ({ ...attributes, id, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts index 530716f709f64..62a542b2b2037 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts @@ -41,7 +41,11 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ try { const editMonitor: SavedObjectsUpdateResponse = - await savedObjectsClient.update(syntheticsMonitorType, monitorId, monitor); + await savedObjectsClient.update( + syntheticsMonitorType, + monitorId, + monitor.type === 'browser' ? { ...monitor, urls: '' } : monitor + ); const errors = await syntheticsService.pushConfigs(request, [ { diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index 2b96ce369294a..ffe4d4ed4bf93 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -43,6 +43,9 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => savedObjectsClient, esClient: esClient.asCurrentUser, }); + + server.uptimeEsClient = uptimeEsClient; + if (isInspectorEnabled) { inspectableEsQueriesMap.set(request, []); } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7f83b1464805a..977d0c3f1768c 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -5,7 +5,8 @@ * 2.0. */ -const alwaysImportedTests = [ +require('../../src/setup_node_env'); +require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/functional_basic/config.ts'), require.resolve('../test/security_solution_endpoint/config.ts'), @@ -25,8 +26,6 @@ const alwaysImportedTests = [ require.resolve('../test/usage_collection/config.ts'), require.resolve('../test/fleet_functional/config.ts'), require.resolve('../test/functional_synthetics/config.js'), -]; -const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config_security_basic.ts'), require.resolve('../test/api_integration/config_security_trial.ts'), require.resolve('../test/api_integration/config.ts'), @@ -93,10 +92,4 @@ const onlyNotInCoverageTests = [ require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), require.resolve('../test/examples/config.ts'), require.resolve('../test/functional_execution_context/config.ts'), -]; - -require('../../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - ...alwaysImportedTests, - ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index a99de22181766..b1ad23170ae07 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -122,7 +122,7 @@ async function alwaysFiringExecutor(alertExecutorOptions: any) { } if (group) { - const instance = services.alertInstanceFactory('1').replaceState({ instanceStateValue: true }); + const instance = services.alertFactory.create('1').replaceState({ instanceStateValue: true }); if (subgroup) { instance.scheduleActionsWithSubGroup(group, subgroup, { @@ -177,8 +177,8 @@ function getCumulativeFiringAlertType() { const runCount = (state.runCount || 0) + 1; times(runCount, (index) => { - services - .alertInstanceFactory(`instance-${index}`) + services.alertFactory + .create(`instance-${index}`) .replaceState({ instanceStateValue: true }) .scheduleActions(group); }); @@ -446,13 +446,13 @@ function getPatternFiringAlertType() { for (const [instanceId, instancePattern] of Object.entries(pattern)) { const scheduleByPattern = instancePattern[patternIndex]; if (scheduleByPattern === true) { - services.alertInstanceFactory(instanceId).scheduleActions('default', { + services.alertFactory.create(instanceId).scheduleActions('default', { ...EscapableStrings, deep: DeepContextVariables, }); } else if (typeof scheduleByPattern === 'string') { - services - .alertInstanceFactory(instanceId) + services.alertFactory + .create(instanceId) .scheduleActionsWithSubGroup('default', scheduleByPattern); } } @@ -538,7 +538,7 @@ function getLongRunningPatternRuleType(cancelAlertsOnRuleTimeout: boolean = true return {}; } - services.alertInstanceFactory('alert').scheduleActions('default', {}); + services.alertFactory.create('alert').scheduleActions('default', {}); // run long if pattern says to if (pattern[globalPatternIndex++] === true) { diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 3a6bc03e29f59..02b391bf1a2d6 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -199,7 +199,9 @@ export default function ({ getService }) { 'primary', 'replica', 'documents', + 'documents_deleted', 'size', + 'primary_size', 'isFrozen', 'aliases', // Cloud disables CCR, so wouldn't expect follower indices. @@ -233,7 +235,9 @@ export default function ({ getService }) { 'primary', 'replica', 'documents', + 'documents_deleted', 'size', + 'primary_size', 'isFrozen', 'aliases', // Cloud disables CCR, so wouldn't expect follower indices. diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index efdf862c3070e..6dd569d891fdc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -941,6 +941,49 @@ export default ({ getService }: FtrProviderContext) => { }); }); + /** + * Here we test that 8.0.x alerts can be generated on legacy (pre-8.x) alerts. + */ + describe('Signals generated from legacy signals', async () => { + beforeEach(async () => { + await deleteSignalsIndex(supertest, log); + await createSignalsIndex(supertest, log); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + }); + + afterEach(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + it('should generate a signal-on-legacy-signal with legacy index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.siem-signals-*`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + + it('should generate a signal-on-legacy-signal with AAD index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.alerts-security.alerts-default`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + }); + /** * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" * in the code). If the rule specifies a mapping, then the final Severity or Risk Score diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e00ea43a02406..417e0c76a9e6b 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -101,6 +101,17 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); + it('should return a 400 with an empty name', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: ' ', + namespace: 'default', + }) + .expect(400); + }); + it('should return a 400 with an invalid namespace', async () => { await supertest .post(`/api/fleet/agent_policies`) diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 8901c3166ca14..57e57a6524b0e 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -174,7 +174,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { @@ -591,7 +591,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 71c0d101943b9..c038aeba608bd 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -14,6 +14,7 @@ import { USER } from '../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); + const browser = getService('browser'); const testUsers = [ { user: USER.ML_POWERUSER, discoverAvailable: true }, @@ -44,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertKibanaNavMLEntryExists(); }); - it('should display tabs in the ML app correctly', async () => { + it('should display side nav in the ML app correctly', async () => { await ml.testExecution.logTestStep('should load the ML app'); await ml.navigation.navigateToMl(); @@ -52,33 +53,60 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { await ml.testExecution.logTestStep('should load the ML overview page'); await ml.navigation.navigateToOverview(); - await ml.testExecution.logTestStep('should display enabled AD create job button'); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection empty state'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(true); - await ml.testExecution.logTestStep('should display enabled DFA create job button'); + await ml.testExecution.logTestStep('should display DFA empty state'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should persist the getting started callout state after refresh' + ); + await browser.refresh(); + await ml.overviewPage.assertGettingStartedCalloutVisible(false); }); }); } @@ -164,6 +192,21 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the Overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection panel'); + await ml.overviewPage.assertAdJobsOverviewPanelExist(); + + await ml.testExecution.logTestStep('should display DFA panel'); + await ml.overviewPage.assertDFAJobsOverviewPanelExist(); + }); + it('should display elements on Anomaly Detection page correctly', async () => { await ml.testExecution.logTestStep('should load the AD job management page'); await ml.navigation.navigateToMl(); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 9abb30548b0eb..fd9cb2cb4c79e 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -52,20 +52,30 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(false); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { @@ -73,11 +83,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToOverview(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should not display ML Nodes panel'); + await ml.mlNodesPanel.assertNodesOverviewPanelExists(false); + await ml.testExecution.logTestStep('should display disabled AD create job button'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(false); await ml.testExecution.logTestStep('should display disabled DFA create job button'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(false); }); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index cb9ef179f0626..d6b75f53578a8 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -334,5 +334,9 @@ export function MachineLearningCommonUIProvider({ await PageObjects.spaceSelector.goToSpecificSpace(spaceId); await PageObjects.spaceSelector.expectHomePage(spaceId); }, + + async waitForDatePickerIndicatorLoaded() { + await testSubjects.waitForEnabled('superDatePickerApplyTimeButton'); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 4b48e4c0269eb..f7fd5efefda33 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -54,6 +54,7 @@ import { MachineLearningDashboardEmbeddablesProvider } from './dashboard_embedda import { TrainedModelsProvider } from './trained_models'; import { TrainedModelsTableProvider } from './trained_models_table'; import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; +import { MlNodesPanelProvider } from './ml_nodes_list'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -124,6 +125,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, api, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context); + const mlNodesPanel = MlNodesPanelProvider(context); return { anomaliesTable, @@ -173,5 +175,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { testResources, trainedModels, trainedModelsTable, + mlNodesPanel, }; } diff --git a/x-pack/test/functional/services/ml/ml_nodes_list.ts b/x-pack/test/functional/services/ml/ml_nodes_list.ts new file mode 100644 index 0000000000000..37cd4143e26cc --- /dev/null +++ b/x-pack/test/functional/services/ml/ml_nodes_list.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MlNodesPanelProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertNodesOverviewPanelExists(expectPanelExits: boolean = true) { + if (expectPanelExits) { + await testSubjects.existOrFail('mlNodesOverviewPanel'); + } else { + await testSubjects.missingOrFail('mlNodesOverviewPanel'); + } + }, + + async assertNodesListLoaded() { + await testSubjects.existOrFail('mlNodesTable loaded', { timeout: 5000 }); + }, + + async assertMlNodesCount(minCount: number = 1) { + const actualCount = parseInt(await testSubjects.getVisibleText('mlTotalNodesCount'), 10); + expect(actualCount).to.not.be.lessThan( + minCount, + `Total ML nodes count should be at least '${minCount}' (got '${actualCount}')` + ); + }, + + async assertNodeOverviewPanel() { + await this.assertNodesOverviewPanelExists(); + await this.assertNodesListLoaded(); + await this.assertMlNodesCount(); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index c11721453d10f..6bf753926c72a 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -106,14 +106,38 @@ export function MachineLearningNavigationProvider({ await this.assertTabEnabled('~mlMainTab & ~anomalyDetection', expectedValue); }, + async assertAnomalyExplorerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~anomalyExplorer', expectedValue); + }, + + async assertSingleMetricViewerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~singleMetricViewer', expectedValue); + }, + async assertDataFrameAnalyticsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataFrameAnalytics', expectedValue); }, + async assertTrainedModelsNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~trainedModels', expectedValue); + }, + + async assertNodesNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~nodesOverview', expectedValue); + }, + async assertDataVisualizerTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataVisualizer', expectedValue); }, + async assertFileDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~fileDataVisualizer', expectedValue); + }, + + async assertIndexDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~indexDataVisualizer', expectedValue); + }, + async assertSettingsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~settings', expectedValue); }, diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts index 8fc04dfa29b18..5f02edde0f310 100644 --- a/x-pack/test/functional/services/ml/overview_page.ts +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -13,6 +13,24 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC const testSubjects = getService('testSubjects'); return { + async assertGettingStartedCalloutVisible(expectVisible: boolean = true) { + if (expectVisible) { + await testSubjects.existOrFail('mlGettingStartedCallout'); + } else { + await testSubjects.missingOrFail('mlGettingStartedCallout'); + } + }, + + async dismissGettingStartedCallout() { + await this.assertGettingStartedCalloutVisible(true); + await testSubjects.click('mlDismissGettingStartedCallout'); + await this.assertGettingStartedCalloutVisible(false); + }, + + async assertADEmptyStateExists() { + await testSubjects.existOrFail('mlAnomalyDetectionEmptyState'); + }, + async assertADCreateJobButtonExists() { await testSubjects.existOrFail('mlCreateNewJobButton'); }, @@ -27,6 +45,14 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertAdJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnomalyDetection'); + }, + + async assertDFAEmptyStateExists() { + await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + }, + async assertDFACreateJobButtonExists() { await testSubjects.existOrFail('mlAnalyticsCreateFirstButton'); }, @@ -41,6 +67,10 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertDFAJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnalytics'); + }, + async assertJobSyncRequiredWarningExists() { await testSubjects.existOrFail('mlJobSyncRequiredWarning', { timeout: 5000 }); }, diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index de0fb1829c2b1..2cd6b50a2062f 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -54,8 +54,8 @@ export const alwaysFiringAlertType: RuleType< const { services, state, params } = alertExecutorOptions; (params.instances || []).forEach((instance: { id: string; state: any }) => { - services - .alertInstanceFactory(instance.id) + services.alertFactory + .create(instance.id) .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) .scheduleActions('default'); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index ebef251984cd6..89f15705beb59 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -177,7 +177,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid producer: 'observability.test', }, services: { - alertInstanceFactory: sinon.stub(), + alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, } as unknown as RuleExecutorOptions< diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 1bd95ca9f16e4..dbcb520c2a5f5 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -153,7 +153,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('editing', () => { + // FLAKY: https://github.com/elastic/kibana/issues/106547 + describe.skip('editing', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.gotoDashboardLandingPage(); diff --git a/yarn.lock b/yarn.lock index dbc92542b3693..ad5df52a1655c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3195,6 +3195,13 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" +"@istanbuljs/nyc-config-typescript@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz#1f5235b28540a07219ae0dd42014912a0b19cf89" + integrity sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + "@istanbuljs/schema@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" @@ -3826,6 +3833,10 @@ version "0.0.0" uid "" +"@kbn/logging-mocks@link:bazel-bin/packages/kbn-logging-mocks": + version "0.0.0" + uid "" + "@kbn/logging@link:bazel-bin/packages/kbn-logging": version "0.0.0" uid "" @@ -6798,6 +6809,14 @@ version "0.0.0" uid "" +"@types/kbn__logging-mocks@link:bazel-bin/packages/kbn-logging-mocks/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__logging@link:bazel-bin/packages/kbn-logging/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__mapbox-gl@link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types": version "0.0.0" uid ""